Running Qiskit on Real IBM Quantum Hardware: Tips and Pitfalls
A practical guide to running Qiskit circuits on real IBM quantum processors: account setup, backend selection, ISA transpilation, session management, reading results, and avoiding common errors on noisy hardware.
Circuit diagrams
Why Real Hardware Matters
Running on a simulator is necessary for development, but it hides everything that makes quantum computing hard in practice: gate errors, decoherence, limited qubit connectivity, readout errors, and queue wait times. Every quantum algorithm paper that claims hardware validation has fought through these issues. This tutorial prepares you for that experience.
Account Setup and API Token
You need a free IBM Quantum account at quantum.ibm.com. After creating an account:
- Navigate to your profile and copy your API token.
- Install the required packages:
pip install qiskit qiskit-ibm-runtime
- Save your token locally (you only do this once per machine):
from qiskit_ibm_runtime import QiskitRuntimeService
QiskitRuntimeService.save_account(
channel="ibm_quantum",
token="YOUR_API_TOKEN_HERE",
overwrite=True,
)
This stores your token in ~/.qiskit/qiskit-ibm.json. Never commit this file or hardcode the token in source code. Use environment variables in production:
import os
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService(
channel="ibm_quantum",
token=os.environ["IBM_QUANTUM_TOKEN"],
)
Selecting a Backend
IBM Quantum gives you access to multiple real devices. Each device has different qubit counts, error rates, connectivity graphs, and queue lengths.
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
# List all available backends
backends = service.backends()
for b in backends:
print(f"{b.name:30s} qubits={b.num_qubits:3d} pending_jobs={b.status().pending_jobs}")
To pick the least busy backend meeting a minimum qubit requirement:
from qiskit_ibm_runtime.fake_provider import FakeManilaV2 # for testing
backend = service.least_busy(
min_num_qubits=5,
operational=True,
simulator=False,
)
print(f"Selected backend: {backend.name}")
For production work you often want a specific backend rather than least busy, especially if you have characterized its error rates or if your circuit requires specific qubit connectivity. Use service.backend("ibm_brisbane") (or whichever device) directly in that case.
ISA Circuits: What They Are and Why They Matter
IBM Quantum hardware only runs ISA (Instruction Set Architecture) circuits: circuits that use only the native gates of the target device and only connect qubits that are physically adjacent on the device.
Your abstract Qiskit circuit almost certainly uses non-native gates (like CX applied to non-adjacent qubits, or gates like CCX that must be decomposed). You must transpile before submitting.
# Requires: qiskit_ibm_runtime
from qiskit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
# Example circuit
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)
qc.measure_all()
# Generate ISA-compliant circuit for your target backend
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
isa_circuit = pm.run(qc)
print(f"Original depth: {qc.depth()}")
print(f"Transpiled depth: {isa_circuit.depth()}")
print(f"Transpiled gate counts: {isa_circuit.count_ops()}")
optimization_level=3 runs the most aggressive optimization passes (Sabre layout, swap reduction, gate merging). It takes longer but produces shorter circuits with fewer errors.
Always check the transpiled depth. A circuit that was 10 gates deep can become 40-80 gates after routing for a connectivity-limited device. If the transpiled circuit is very deep, consider whether your algorithm can be restructured to match the device topology better.
Session Management
IBM Quantum Runtime uses sessions to keep your jobs prioritized and batched together on the same device, reducing queue overhead for circuits that depend on each other.
from qiskit_ibm_runtime import Session, SamplerV2 as Sampler
with Session(backend=backend) as session:
sampler = Sampler(mode=session)
# Submit multiple related jobs in the same session
job1 = sampler.run([(isa_circuit,)], shots=1024)
job2 = sampler.run([another_isa_circuit], shots=1024)
result1 = job1.result()
result2 = job2.result()
Sessions are particularly useful for variational algorithms (VQE, QAOA) where you submit many parameterized circuit variants during optimization. Without a session each job re-enters the general queue independently.
For single one-off submissions you can run without a session:
from qiskit_ibm_runtime import SamplerV2 as Sampler
sampler = Sampler(mode=backend)
job = sampler.run([(isa_circuit,)], shots=2048)
print(f"Job ID: {job.job_id()}")
Save the job ID. If you lose your connection, you can retrieve the result later:
# Requires: qiskit_ibm_runtime
job = service.job("YOUR_JOB_ID_HERE")
result = job.result()
Queue Wait Times
IBM Quantum queues can range from seconds to hours depending on the backend and time of day. A few strategies:
- Use
least_busy()for experimentation. It often gives wait times under 5 minutes. - Run small jobs (under 100 circuits per session) to avoid being deprioritized.
- Schedule jobs at off-peak hours. US peak times are roughly 9 AM to 6 PM Eastern.
- Check backend status before submitting:
backend.status()shows pending job count. - Prefer simulators (like
FakeManilaV2or local Aer simulation) for debugging. Only submit to real hardware when you specifically need hardware results.
Reading Job Results
The result format changed significantly in Qiskit 1.x. Using SamplerV2:
# Requires: qiskit_ibm_runtime
result = job.result()
# For a single circuit submission
pub_result = result[0] # First PUB (Primitive Unified Bloc)
data = pub_result.data
# Access bit arrays by classical register name
counts = data.meas.get_counts()
print(counts)
# Or get raw bit array
bit_array = data.meas.get_bitstrings()
A common mistake is using the old get_counts() on the top-level result object. That API is gone in Qiskit 1.x. Always index into result[i] first.
Common Errors and Pitfalls
Connectivity mismatch. You try to apply a CX gate between qubits 0 and 4, but they are not adjacent on the device. The runtime rejects the circuit. Fix: always transpile before submitting. Never submit abstract circuits directly to hardware.
T-gate fidelity. The T gate is not native on most IBM devices. It gets decomposed into native gates, adding error. Circuits that use many T gates (common in fault-tolerant algorithms) are especially noisy. Check isa_circuit.count_ops() to see how many native gates your circuit uses after decomposition.
Shot noise. With 1024 shots, rare measurement outcomes can appear zero times by chance. If your algorithm requires detecting a small-amplitude state, you may need 8192 or more shots. For circuits where the probability of the correct outcome is p, you need roughly 1/(p^2 * delta) shots to distinguish it from noise at confidence level 1 - delta.
Measurement error. Readout fidelity on real devices is typically 95-99% per qubit. For a 5-qubit circuit the probability of at least one bit flip is 5-25%. Use mthree (M3) or Qiskit’s measurement calibration to mitigate readout errors.
Job cancellation. Very long circuits (depth > 1000 on current devices) are often cancelled by the runtime. Keep circuits short by decomposing them or using error mitigation instead of fault-tolerant repetition.
Cost Estimation
IBM Quantum is currently free for open-plan users, with access to smaller devices and limited monthly runtime seconds. Premium plan users get priority access and larger devices. Check your plan’s limits at quantum.ibm.com/account.
A rough rule for budgeting runtime: a 100-qubit circuit with 1024 shots takes approximately 1-5 seconds of quantum runtime. Sessions add overhead. Always prototype with fewer shots and small circuits before scaling up.
A Complete End-to-End Example
# Requires: qiskit_ibm_runtime
import os
from qiskit import QuantumCircuit
from qiskit_ibm_runtime import QiskitRuntimeService, Session, SamplerV2 as Sampler
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
service = QiskitRuntimeService(
channel="ibm_quantum",
token=os.environ["IBM_QUANTUM_TOKEN"],
)
# Build a simple Bell state circuit
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
# Select backend and transpile
backend = service.least_busy(min_num_qubits=2, operational=True, simulator=False)
print(f"Using backend: {backend.name}")
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
isa_qc = pm.run(qc)
print(f"Transpiled circuit depth: {isa_qc.depth()}")
# Run with session
with Session(backend=backend) as session:
sampler = Sampler(mode=session)
job = sampler.run([isa_qc], shots=1024)
print(f"Job submitted. ID: {job.job_id()}")
result = job.result()
counts = result[0].data.meas.get_counts()
print(f"Counts: {counts}")
# Expected (with noise): mostly '00' and '11', with small '01' and '10' error rates
This end-to-end flow covers every step. Once you have this working, you can substitute any circuit for qc and follow the same pattern.
Was this tutorial helpful?