Building an n8n Workflow That Retries a Failed Claude API Call With Exponential Backoff

Close-up of an old gear mechanism, illustrating a step-by-step automated retry loop

n8n’s built-in “Retry On Fail” setting only supports one fixed wait time between attempts, not exponential backoff — there’s no field for it in the node’s Settings tab. To actually back off exponentially against Claude API rate limits, you have to build the retry loop yourself with a Wait node and an IF node, reading the attempt count from a counter you maintain. This is one way to do it, including the one piece most backoff tutorials skip: checking the API’s own retry-after header before falling back to a calculated delay.

What “Retry On Fail” actually gives you

n8n’s own documentation on handling rate limits describes two built-in options on the HTTP Request node, and neither one is exponential. The first is the Retry On Fail toggle under Settings, paired with a Wait Between Tries (ms) field — a single fixed number, with the documentation’s own example being “for an API that allows one request per second, use 1000.” The second is the node’s Batching option, which spaces out multiple items with a fixed delay between requests, not between retries of the same failed one. Both are fine for a steady rate limit. Neither adapts when Claude’s API asks you to back off harder, because a fixed wait is the same whether it’s your first retry or your fifth.

Why a fixed wait fights the way Anthropic’s limits actually work

Anthropic documents its API rate limits as a token bucket that “replenishes continuously rather than resetting at fixed intervals,” and warns that a 60 RPM limit “may be enforced as 1 request per second” — meaning a burst of retries fired on a fixed short delay can re-trip the same limit it’s trying to recover from. The same page’s guidance is specific: “when retry-after is present, wait that many seconds before retrying,” and more generally, “increase your traffic gradually and maintain consistent usage patterns.” A retry loop that doubles its wait on every attempt, and defers to the header when the API provides one, follows both of those instructions. A fixed 1-second retry loop follows neither.

The workflow structure

Five nodes handle the whole loop. The pattern is: call the API, check if it failed, and if it did and you haven’t hit the attempt limit, wait and loop back to the same HTTP Request node.

  1. Set — Init attempt counter. Add a field attempt set to 0 before the loop starts.
  2. HTTP Request — Call Claude. POST to https://api.anthropic.com/v1/messages. Under Settings, turn on Continue On Fail — without it, a non-2xx response stops the whole workflow before your retry logic ever runs. Under Options, enable “Include Response Headers and Status” so the node’s output carries the status code and the retry-after header instead of just the parsed body.
  3. IF — Did it fail? Branch on {{ $json.statusCode }} >= 400 (or check for the error object Continue On Fail attaches). The “true” branch below handles the retry; the “false” branch continues on to whatever uses the Claude response.
  4. IF — Attempts left? Branch on {{ $json.attempt }} < 5 (or whatever ceiling fits your use case). If this is false, route to an error-handling path instead of looping forever.
  5. Wait, then increment, then loop back. A Wait node set to "After Time Interval" with the expression below, followed by a Set node that does {{ $json.attempt + 1 }}, connected back into the HTTP Request node from step 2.

The Wait node expression: header first, exponential second

This is the piece that makes the loop match Anthropic's documented behavior instead of guessing at a delay. Anthropic's own minimal example for retry timing checks for a retry-after header before falling back to a calculated wait, and the n8n Wait node's "Resume: After Time Interval" field accepts an expression, so the same logic fits in one line:

{{ $json.headers['retry-after'] ? Number($json.headers['retry-after']) : Math.min(2 ** $json.attempt, 60) }}

If Anthropic sent a retry-after header, use that value in seconds — the documentation is explicit that "earlier retries fail" if you don't. If it didn't, fall back to doubling: 1 second, 2, 4, 8, capped at 60, indexed by the attempt counter from the Set node. The cap matters as much as the doubling — without one, five failed attempts in a row would ask the Wait node to pause for 16 seconds, then 32, and the workflow execution timeout (or the person waiting on the result) runs out before the backoff does.

Fixed delay vs. backoff, side by side

Retry On Fail (built-in)Backoff loop (this setup)
Wait between attempt 1 and 2Same fixed value every time1 second (or the retry-after value, if present)
Wait between attempt 4 and 5Same fixed value every time8 seconds (or the retry-after value, if present)
Respects a documented retry-after headerNo — the field is a static number set at design timeYes — read from the response on every attempt
Where attempt count is visibleNot exposed to the workflowIn the Set node's attempt field, usable in logs or an alert

The built-in setting is still the right call for a steady, predictable rate limit where retries are rare and short. The loop earns its extra five nodes when the workflow calls Claude inside a burst — a batch of rows processed one at a time, for instance — where the first request's rate-limit response is a signal that the next several are about to hit the same wall at the same fixed delay, instead of spacing themselves out.

What Continue On Fail changes about the node's output

Turning on Continue On Fail changes what step 3's IF node has to check. Without it, a failed HTTP Request simply stops the branch — there's no item left to inspect. With it on, the node passes through an item whose JSON carries an error property instead of (or alongside) the usual response body, and — because "Include Response Headers and Status" is also on — a statusCode field and a headers object sitting next to it. That's what step 3's condition and the Wait node's expression are both reading from. It's worth opening the node's output panel after one deliberate failed run and looking at the actual JSON shape before wiring the IF conditions, since the exact property names can shift slightly between n8n versions and HTTP Request node settings — the Retry-After header name itself may come through as retry-after or Retry-After depending on how the upstream proxy normalizes it, and an expression that only checks one case will silently fall through to the exponential branch on the other.

Testing the loop without waiting for a real rate limit

You don't want to find out the Wait expression has a typo by burning through your actual Claude rate limit five times in a row. Point the HTTP Request node at a throwaway endpoint that returns a controllable 429 — a request-echoing service like httpbin can be configured to return an arbitrary status code and headers, including a fake retry-after value, which lets you confirm three things before switching the URL back to Anthropic's endpoint: the IF node in step 3 actually routes on a 429 the way it routes on a 401 or 500, the Wait node reads a retry-after header when one is present instead of always falling through to the exponential calculation, and the attempt counter actually increments instead of resetting to 0 on every loop (a common mistake is wiring the Set node in step 1, which only runs once, into the retry path, instead of a separate increment step that runs on every pass). Five minutes against a mock endpoint catches all three before they cost you real request budget.

One thing this loop still doesn't do

Anthropic's rate-limit documentation separately describes a spend-cap 429 — a response with the same rate_limit_error type but no retry-after header, distinguished by error.details.error_code == "enforced_spend_limit_reached". Retrying that one, on any schedule, "fails until access resumes" at the time stated in the error message. The IF node checking "attempts left" in step 4 should also check for that error code and route straight to the failure path, skipping the wait entirely — a backoff loop that doesn't know the difference will burn through its five attempts waiting out a cap that was never going to lift in that window.

If this workflow is also the one logging Claude API costs to a spreadsheet, log the attempt count alongside the cost — a prompt that needed four retries to get through is worth noticing before it becomes five, and the same reasoning that explains why a Zapier step times out against Claude applies here too: a platform's own request timeout doesn't care how patient your backoff is, so check your automation tool's execution ceiling before setting the attempt limit high enough to need it. For the exact pricing-tier limits behind the 429s you're retrying, the rate limit and pricing tier comparison covers what changes as usage scales, and if the same job is calling an AI outreach tool on the other end, remember that a second rate limit downstream can fail a workflow that handled Claude's limit perfectly.

Featured image: "Abandoned concrete factory mechanism" by Diacritica, licensed CC BY 3.0, via Wikimedia Commons.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top