Skip to content

One circuit, two IBM Quantum error codes: 1506 and 1603

20 identical submissions, 11 of one code and 9 of the other. Which one you get decides whether IBM tells you what is wrong.

results.ts open in VS Code, showing the sample parsing functions in the n8n-nodes-ibm-quantum source tree
results.ts in the n8n-nodes-ibm-quantum repository. Every result the node returns is parsed here, including the one that came back empty for a job IBM had answered in full.

I submitted the same rejected program to IBM Quantum 20 times in a row. Same device, same 64 shots, same tag, one stored workflow fired 20 times so the payload could not drift. 20 submissions in 19 seconds.

11 came back as reason code 1506. 9 came back as 1603. Interleaved, not in 2 blocks.

1603 1603 1506 1603 1506 1603 1506 1506 1506 1603
1603 1603 1506 1506 1506 1506 1603 1506 1506 1603

2 and a half hours earlier that same evening I had seen the same program return 1506 once and 1603 once, 5 minutes apart, and I nearly published that as "IBM is not deterministic". It would have been true and useless. The 20 runs say something much more specific, and they say it with no exceptions.

This is release 0.5.0 of n8n-nodes-ibm-quantum, my community node for running quantum circuits on IBM hardware from inside an n8n workflow: 69 jobs on real hardware during the verification campaign, 98 including the replication below, 574 of the Open plan's 600 QPU seconds, 26 problems found. The one worth writing about is the one where the wrong answer was mine, twice.

The control I ran could only agree with me

3 weeks ago I put rzz on real hardware, watched IBM fail it, and published the result. The FAQ on my previous postanswers "Can I use rzz on IBM hardware through OpenQASM 3?" with "Not through the REST API as of August 2026."

That answer is wrong, and it was wrong the day I published it. If you read it and routed around a gate you could have used, that one is on me.

The mechanism is more useful than the apology. rzz is a 2 qubit rotation and it sits in the basis gate set the 3 Heron devices I can reach all report: cz, id, rx, rz, rzz, sx, x. When a submission using it failed, I ran what I thought was a control. I removed rzz, submitted the rest, and watched it complete. That proved the failure belonged to rzz rather than to the surrounding circuit.

It could not prove anything else. A control that removes the suspect can only tell you the suspect is involved. It cannot separate rzz is rejected from rzz written this particular way is rejected, and the distance between those 2 sentences is this entire release.

What makes it worse is that I had already done it properly, in the same campaign, on a different gate. The id finding in that post came from 3 control jobs varying one thing at a time, and I wrote a line about it I now get to eat: a theory that sounds right is the most expensive kind of wrong, so I put it on hardware. I did put it on hardware. I put the wrong half of it on hardware.

What rzz does when you define it

The variable I never varied was the definition block.

stdgates.inc, the OpenQASM 3 standard library, defines no rzz. My circuit builder writes bare gate calls with no definitions, so what it emitted was a call to something the loader had never heard of. Qiskit's own exporter writes the definition alongside the call, here with readable parameter names in place of the exporter's generated ones:

gate rzz(p0) a, b { cx a, b; rz(p0) b; cx a, b; }
rzz(0.5) q[0], q[1];

Submitted that way, the job completes. The gate was never the problem. The missing definition was. 2 different bugs, 2 different fixes, and I had spent 3 weeks fixing the wrong one, up to and including deleting a working implementation.

Then I wrote those same 3 instructions out by hand, without the wrapper, and submitted that:

cx q[0], q[1];
rz(0.5) q[1];
cx q[0], q[1];

It failed. Circuit 0: The instruction cx on qubits (0, 1) is not supported by the target system, reason code 1517. cx is not in the Heron basis, and nothing rewrote it.

So the identical 3 instructions run inside a gate rzz block and are refused when written flat. The loader is not inlining the definition and passing the result to the target. Either it maps the body onto the device basis, or it recognises the gate by name and substitutes the native RZZ.

I know exactly which experiment separates those 2, and I have not run it. Submit the same body under a name IBM's basis does not contain, gate myzz(p0) a, b { cx a, b; rz(p0) b; cx a, b; }. If it completes, the loader maps bodies. If it returns 1517, it substitutes by name. That is one job and about 2 seconds of quota, I had 26 seconds left when I finished the runs below, and I am writing this before spending them. Naming the control I did not run is the least I can do in a post about the control I ran badly.

The timing cannot decide it either. The rzz circuit and a bare cz baseline both report circuits_execution_time_ns of exactly 16080128, identical to the nanosecond, and the 3 defined jobs in the alternation run report the same number again. That is either a quantised metric or 2 circuits that really are the same shape at the hardware, and 5 identical integers cannot tell me which.

What it does rule out is the naive reading, which is that nothing happens to your circuit between submission and execution. I have been telling people Qiskit Runtime does not transpile. That is true in the way that costs you money, since it will not route your qubits onto a coupling map and it will fail a circuit that needs it. It is not true that the program reaches the hardware untouched.

One more thing worth knowing before you reach for the gate. IBM documents fractional gates as rzz for angles in the range 0 to π/2 and rx for any angle, on Heron only, marked experimental, and incompatible with Pauli twirling, with probabilistic error cancellation, and with zero noise extrapolation by error amplification. I tested one angle, 0.5 radians, well inside that range. And by my own argument above I cannot even say a fractional gate ran, because what completed carried the cx, rz, cx body.

Two codes, two paths, and only one of them tells you why

Back to the 20 jobs.

IBM publishes an error registry. It titles 1506 "QASM parsing failed" and 1603 "Error loading QASM circuit with the standard Qiskit loaders". I had not read it before the campaign, and I have no excuse, because the 1506 message ends with a link to that registry. IBM printed the correction inside the error I was reading.

But the registry only names the codes. The job metrics say which one a job is going to get, and the split is total:

1603 1506
jobs 9 11
got a running timestamp 0 of 9 11 of 11
time from created to finished 0.401 to 0.891 s 1.873 to 2.628 s
median 0.545 s 2.264 s

The 2 time ranges do not overlap anywhere. 0.98 s separates the slowest 1603 from the fastest 1506, the medians differ by a factor of 4, and an exact rank sum test puts a separation this clean, in the predicted direction, at about 1 in 168,000.

And the slower code is exactly the set that got dispatched.

So: a submitted job is validated, then dispatched. If the validator catches the undefined gate, the job dies in about half a second with 1603. If it does not, the job is dispatched, gets a running timestamp, and the execution side refuses it about 2 seconds later with 1506. Both codes are correct descriptions of what happened to that particular job. The program is identical. Which component got to it first is not.

2 limits on that model, both from my own records. It is what this circuit did on this device on this evening. And my changelog has a 1603 that arrived about 15 seconds after a job was queued, for duplicate bit arguments, on which I never read the metrics. So treat "validation stage" as what this experiment showed for this failure, not as a property of the code.

The interleaving rules out a single atomic cutover: a deployment landing mid run produces 2 blocks, and 20 jobs produced 11 alternating runs. It does not rule out a rollout across replicas, although the same mixture at 17:29 and again at 20:03, 2 and a half hours apart, makes a 19 second rollout unlikely.

The 2 codes do not carry the same message, and the difference is not cosmetic.

The 1603 message names the cause down to the line and column:

Job not valid. Error loading QASM circuit with the standard Qiskit loaders:
"5,0: gate 'rzz' is not defined.

The 1506 message does not mention rzz. It does not mention any gate. It offers a link to the OpenQASM guide:

Failed to execute program: 'Error parsing OpenQASM program:  -- Pass valid
OpenQASM strings that can be imported into Qiskit. ...

Anyone debugging a 1506 has, in that message, no way to learn that one undefined gate was the problem. Resubmitting the identical program is close to a coin flip: I saw 9 of 20, but 20 jobs put that anywhere between about a quarter and 2 thirds, so treat it as worth one retry rather than as a rate. It is not something you would guess from either message on its own.

None of this destabilises the node, for a reason worth stating plainly: it never branches on reason_code. The check that matters runs before submission and warns when a circuit calls rzz without defining it, which is the one gate in the Heron basis that stdgates.inc does not define. That warning fired on all 20 submissions before any of them left my machine. The node explained the failure correctly 20 times out of 20. IBM explained it correctly 9.

A rejected job is not free

All 20 jobs failed. Not one executed a shot: circuits_execution_time_ns is 0 on every one of them.

The account was billed 30 seconds.

15 of the 20 carried a charge of 2 seconds, and the quota moved by exactly that. The 5 that were not charged were all validation stage failures, the ones that never got a running timestamp. Across the whole evening, 29 jobs cost 48 seconds of an allowance that renews once every 28 days.

My own repository, from the 0.4.1 audit, says no QPU time is charged for an invalid circuit. It cited running: null and an unchanged quota as evidence. That measurement was right about the case it measured: every job here with running: nullcost nothing, all 5 of them. What it got wrong was the generalisation, because it never met the 15 jobs that were dispatched and charged. The claim comes out.

If you are on the Open plan, price a debugging loop accordingly. 20 attempts at a circuit that cannot possibly run is 5 percent of your month.

Every endpoint that took whatever you typed

The most consequential thing in this release has nothing to do with quantum computing.

There was no encodeURIComponent anywhere in the package. Every endpoint that takes an identifier interpolated it straight into the request path. Confirmed live: a Job ID of ../backends returned the device list, a Session ID of ../jobsreturned the job list, and ../../instances escaped the /v1 prefix entirely.

The blast radius included DELETE /jobs/{id} and PUT /jobs/{id}/tags. The node is also exposed as an AI Agent tool, which means those identifiers routinely arrive from a language model rather than from a person typing into a field.

Every identifier is encoded now, bounded, and validated as text before it becomes a path segment. A value of nothing but dots is refused outright, because URL resolution removes such a segment and encoding does not save you: Node decodes %2E before it removes dot segments.

The neighbouring fix is smaller and funnier. With jobId empty, the request was GET /api/v1/jobs/, and IBM's edge answers that with its own web application: HTTP 200, 285 KB of text/html, handed back as a successful job status. An expression like {{ $json.jobId }} on an item missing the field produces exactly that.

The fix pass wrote 4 new defects while every test stayed green

Once the fixes were in, I pointed 34 agents at the change set with one instruction: break it. Every break they claimed went to a second agent whose instruction was to refute it.

They found 4 documentation defects the fix pass itself had written, hours old. The README still told the reader to use Account > Set Cost Limit, an operation the same release had deleted. A screenshot caption still read "Actions (34)". Alt text still said "the nine Account actions" when there were 8. A session finding I had corrected in the node's own field description had never reached the README.

623 tests were passing at that point. Lint was clean. Coverage was 100 percent on statements, branches, functions and lines. A test suite cannot read the README.

I should say what that instrument is worth, since I am quoting its output. 34 runs against the same repository are correlated draws, not independent reviewers, and it shows: they found README text and no code defects. None of them noticed that my rzz control was the wrong shape, or that both error codes were sitting in a public registry. And an earlier run of the same setup died on a session limit and reported "no breaks found", which actually meant that no agent had survived long enough to report anything. A verification instrument returning a false green, in a release about verification instruments returning false greens.

The best of the 20 documentation defects across this release is one the panel showed the user twice, contradicting itself in a single screen. The operation description said Set Accepting Jobs turns acceptance on or off "without closing the session". The field description directly beneath it said the opposite. Measured on a fresh, empty batch session on ibm_fez:

moment state state_reason closed_at
after create open session_created absent
after setAccepting false closed session_closed_early stamped

False closes the session, true does not reopen it, and both descriptions now say that.

The defect that lived between 2 test files

IBM's Noise Learner primitive returns a completed job under a different top level key than everything else:

primitive top level keys in IBM's response pubs the node found resultsAvailable
Sampler ["metadata", "results"] 1 absent
Estimator ["metadata", "results"] 1 absent
Noise Learner ["data", "metadata"] 0 false

My parser read results. A learning job that had finished successfully came back as pubCount: 0pubs: []resultsAvailable: false, and my own documentation defines that last field as meaning IBM sent no result body at all. IBM had sent 1133 bytes containing a complete LayerError: 15 Pauli generators and their rates.

The suite covered the Noise Learner submit path in 10 places. The results file had no Noise Learner case at all, and the resultsAvailable tests only ever covered an empty body. The defect sat in the gap between them, on lines coverage reported as fully exercised.

The gap existed because on the previous campaign I submitted a learning job and cancelled it to save quota. The submit path got proved. The result path was never reached.

Decoded outside the node with numpy, to check the data was real rather than merely present, from a job on ibm_fez:

strat cz, qubits 0 and 1
  ZZ   1.669e-02   <- the characteristic error of a CZ gate
  ZX   5.260e-03
  IX   5.260e-03
  XZ   3.100e-03
  XI   3.100e-03
  the remaining 10 generators: 0
approximate layer fidelity: 0.935

A CZ gate's error profile with the ZZ term dominating, which is what it should look like. The node now emits one noiseLearner pub per learned layer. The rates stay base64 encoded zlib, because inflating them needs zlib and the n8n community node import allowlist does not include it, so decoding would fail official verification.

The rest of 0.5.0

A twenty qubit circuit diagram with an X gate on every even qubit and a measurement on all twenty qubits, writing into a twenty bit classical register
The load circuit. An X on the even qubits, all twenty measured into c. Because c[0] is the rightmost bit, the correct reading is 01010101010101010101, not the other way round.
The n8n Get Results node showing job da4jn8c3jnrc73agammg completed, 500,000 shots on a twenty bit register, with the alternating bitstring at 394,214 counts
Get Results on the 500,000 shot job. 5.25 MB of JSON from IBM, 626 distinct bitstrings, and the expected pattern sitting at 394,214 counts.

sx joins the palette, taking it from 24 gates to 25, and the reason it had been left out was also mine and also wrong. I believed it had no OpenQASM 3 spelling avoiding the builtin Ustdgates.inc defines it. That matters more than one gate normally would, because sx is the only non-fractional single qubit rotation in the basis besides xrz is a frame change and id does nothing, so without sx a hand written ISA circuit had to spell its Hadamard with a parametrised rx, which is fractional, which rules out gate twirling and the mitigation behind Estimator resilience level 2.

Every palette entry now says whether IBM runs it untouched. 7 read "Runs as-is", 16 read "Transpile first" with the reason, 2 are special cases. 16 of 25 is most of the palette. Nothing in the dropdown said so before. A test holds those descriptions against the node's own ISA scanner, so a gate added later with the wrong blurb fails the build.

A coupling map check, because picking gates from the basis is only half of building an ISA circuit and the qubits have to be adjacent too. It is not a naive adjacency test: cz(3,16) is far apart but coupled and completes, while cz(15,16) is consecutive but uncoupled and fails at IBM with code 1517.

Account > Set Cost Limit is gone, leaving 33 operations rather than 34. PUT /v1/instances/configuration hangs for about 180 seconds and then aborts, reproduced twice and once more with the node taken out of the picture entirely, by sending the identical request through n8n's own HTTP Request node. GET on the same path answers in 832 ms. IBM's OpenAPI spec flags the path as deprecated in favour of the Resource Controller API, but there is no replacement for the write, which is why the operation is gone rather than re-pointed. Set the ceiling on the IBM Cloud console; Get Configuration and Get Instance still read it back.

The changelog carries the other 55 entries, including the batching, the backend dropdowns and 6 field descriptions rewritten to say what IBM measurably does.

What I am not claiming

That the split is 55 to 45 in general. It is 11 to 9 in one burst on one device on one evening, consistent with a coin flip and nowhere near enough to pin a rate.

That the race is between exactly 2 components. The running flag and the timings show 2 paths. They do not show the internals of either, and they do not rule out a rollout across replicas.

That order never matters. 6 alternating jobs, bare and defined, showed no effect: all 3 bare returned 1506 and all 3 defined completed. 3 draws cannot prove independence. What they do is remove the confound from my original observation, where a definition carrying job had run between the 2 I called identical.

That the loader decomposes rather than substitutes. That experiment rules out "nothing happens" and leaves 2 live possibilities, and the control that separates them is written out above and unrun.

One thing is still open in the node. A Noise Learner circuit with more than one entangling layer fails on the default settings, because the circuit builder puts a classical register in and the learner never measures your circuit. Set Number of Classical Bits to 0 and the same circuit completes with 2 learned layers. The behaviour is IBM's, the node warns at submit time, and whether the default should change is a decision I have not made.

And 3 things I cannot test from this account at all: the eu-de region, session dedicated mode, which the Open plan refuses, and the Get Results timeout path, because no queue was deep enough during the campaign.

The run in numbers

jobs on real hardware 98 across 2 days, 69 of them before the replication
QPU seconds spent 574 of the Open plan's 600 per 28 days
the replication 20 identical submissions in 19 seconds, 11 codes to 9
devices ibm_fez, ibm_kingston, ibm_marrakesh, all Heron, 156 qubits
problems found 26 in the product, 25 fixed, 1 open, plus 4 on IBM's side
documentation defects 20, 4 of them written by the fix pass itself
tests 669 at the end, up from 616
operations 33, down from 34
changelog entries 62

Questions people ask

Why did the same IBM Quantum circuit return reason_code 1603 and then 1506?

Because the 2 codes come from 2 different stages. A job is validated, then dispatched. In my runs the validator caught an undefined gate in about half a second and reported 1603, while a job that got past it was dispatched, got a runningtimestamp, and was refused about 2 seconds later with 1506. In 20 identical submissions I got 11 1506 and 9 1603, interleaved, with every 1506 having entered the running phase and no 1603 doing so.

What does reason_code 1506 mean on IBM Quantum?

IBM's registry titles it "QASM parsing failed". In my runs it was the rejection that arrived from the execution side after the job had been dispatched, and its message was generic: it named no gate and no line. One cause is calling a gate with no definition, such as a bare rzz.

What does reason_code 1603 mean on IBM Quantum?

IBM titles it "Error loading QASM circuit with the standard Qiskit loaders". In my runs it arrived in under a second without the job ever entering the running phase, and its message named the cause down to the line and column, for example 5,0: gate 'rzz' is not defined. It is also what you get for duplicate bit arguments, such as cx q[0], q[0].

My IBM Quantum error message says nothing useful. Should I retry?

If you got 1506 and the message named no gate, it is worth one retry. The same program can be caught by the validator instead, which returns 1603 with a message that names the gate, the line and the column. I saw that in 9 of 20 submissions, which is close enough to a coin flip that I would not plan around a rate. Budget for it: each attempt can cost about 2 seconds of QPU allowance even though nothing runs.

Can I use rzz in an OpenQASM 3 circuit on IBM hardware?

Yes, if you ship the definition with it. rzz is in the Heron basis set but stdgates.inc does not define it, so a bare rzz(0.5) q[0], q[1]; is refused before the target is consulted. Include gate rzz(p0) a, b { cx a, b; rz(p0) b; cx a, b; } and the same call completes. This corrects the answer in my previous post, which said rzz could not be used through the REST API. What I can say is that the job completes. Whether the hardware runs the native RZZ or the cx, rz, cx body mapped onto the basis, I could not determine. Note also that rzz is a fractional gate: Heron only, angles from 0 to π/2, experimental, and incompatible with Pauli twirling, PEC and ZNE by error amplification.

Do failed IBM Quantum jobs use up my monthly quota?

Yes, including jobs that never execute a shot. 15 of my 20 rejected circuits were charged 2 seconds each, all of them jobs that had been dispatched; the 5 that died in validation were charged nothing. Together they cost 30 seconds of my Open plan allowance while executing 0 shots.

Does Qiskit Runtime transpile my circuit for me?

Not in the way that would save you. It will not route your qubits onto a coupling map, and it will accept a circuit it cannot run, queue it, and fail it seconds or minutes later depending on the queue. But it does not hand your program to the hardware untouched either: 3 instructions inside a gate rzz definition run, while the identical 3 written out flat are refused with code 1517 because cx is not in the basis.

What are the basis gates on IBM Heron processors?

The REST API reports cz, id, rx, rz, rzz, sx, x, identical on all 3 devices I can reach, plus measure, reset, delay and barrier. Be careful with that list, because it is the fractional inclusive superset. Qiskit gives you a narrower conventional set, cz, x, rz, sx, id, unless you load the backend with use_fractional_gates=True, so a gate set taken from the API can hand you fractional gates you never chose. 2 further caveats that cost me jobs: rzz needs a definition block, and id is listed but the node emits nothing for it, because stdgates.inc defines it as U(0, 0, 0) and a job emitting that builtin fails on the target. Why x, also defined through U in the same file, resolves fine is something I have not established.

Why does a completed IBM Noise Learner job return zero pubs?

Because Noise Learner returns its payload under data while Sampler and Estimator return theirs under results. A parser reading only results reports 0 pubs for a job IBM answered in full. Fixed in n8n-nodes-ibm-quantum 0.5.0.

The control, not the calendar

The tidy version of this post is that measurements have a shelf life and mine went stale. It would have been a nicer ending and it is not true. A bare rzz failed on 2 August and a bare rzz failed 20 times on 22 August. Nothing decayed. IBM did not change anything underneath me.

What was wrong was never the measurement. It was the inference I drew from one observation, using a control that could only agree with me, and then a second inference from 2 observations that called a mechanism random because I had not looked at it long enough to see the shape.

The thing that separated them took 19 seconds and 30 seconds of quota.

The last post in this series ended on the line that every defect came from trusting a layer: green tests, covered lines, a vendor's gate list. This one has a narrower and more embarrassing version. 1 job is an observation. 2 jobs are a coincidence. 20 jobs, submitted with nothing in between and read against their own timestamps, is a measurement. I had been shipping on observations and calling the surprises nondeterminism.

Where to find it

The node is n8n-nodes-ibm-quantum on npm, MIT licensed, and it installs from the editor canvas on n8n Cloud and self hosted. The source and the full changelog are on GitHub, along with the verification report and the raw replication data behind this post: every job ID, timestamp, code and message, so you can check the split yourself.