n8n Rate Limit: Fix HTTP 429 with Retry-After + Free Demo

Fix an n8n rate limit error with Retry-After, safe retries and bounded backoff. Download two tested workflow labs and learn when a Wait node is not enough.

A workflow can run perfectly with five items and fail when you send fifty. The API starts returning HTTP 429, n8n shows an error, and a quick retry sometimes makes the problem worse.

The fix depends on what the API is telling you. A fixed pause can help with a simple burst. A response containing Retry-After needs a wait based on that response. A quota shared by several workflows needs coordination across those workflows.

How to fix an n8n HTTP 429 rate-limit error

  1. Inspect the response: turn on Include Response Headers and Status in the HTTP Request node and read the status, body and quota scope.
  2. Reduce the burst: lower the batch size and pace requests to the provider’s documented allowance.
  3. Wait before retrying: honor a valid Retry-After value; use bounded backoff only when that header is missing or invalid.
  4. Protect the job: retain its ID, cap total attempts and retry writes only when repeating them is safe.

To fix an n8n rate limit error, reduce request frequency and inspect the API’s HTTP 429 response. If it includes Retry-After, wait at least that long before a safe retry. Otherwise, use bounded backoff. Keep the original job, limit attempts, and coordinate workflows that share one provider quota.

Copy the tested retry workflow into n8n

No ZIP is needed for the timed demo. Open the panel below, copy the complete JSON, click a blank n8n canvas and paste it with Ctrl+V (Cmd+V on Mac). Run it with Execute workflow. The expected final output is PASS after a simulated 429 → 200 sequence and a real wait of at least two seconds.

This is the same credential-free teaching workflow used in our test. It makes no external requests. The full lab download also contains the 18-case fixtures and setup notes.

Show complete workflow JSON — select all inside this panel and copy
{
  "name": "OperantSolo - 429 Retry-After loop - offline demo",
  "nodes": [
    {
      "id": "run-loop-demo",
      "name": "Run loop demo",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "parameters": {},
      "position": [
        120,
        360
      ]
    },
    {
      "id": "prepare-demo-job",
      "name": "Prepare demo job",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "return [{json:{attempt:1,maxAttempts:4,baseSeconds:2,maxInlineSeconds:300,safeToRetry:true,request:{method:'GET',jobId:'offline-demo-001'},history:[]}}];"
      },
      "position": [
        340,
        360
      ]
    },
    {
      "id": "simulate-429-then-200",
      "name": "Simulate 429 then 200",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "return $input.all().map(({json:j},i)=>({json:j.attempt===1?{statusCode:429,headers:{'retry-after':'2'}}:{statusCode:200,headers:{},body:{ok:true}},pairedItem:{item:i}}));"
      },
      "position": [
        780,
        280
      ]
    },
    {
      "id": "decide-next-step",
      "name": "Decide next step",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "function retryPolicy(job, nowMs = Date.now(), random = Math.random) {\n  const response = job.response || {};\n  const status = Number(response.statusCode);\n  const attempt = Number(job.attempt);\n  const maxAttempts = Number(job.maxAttempts ?? 4);\n  const baseSeconds = Number(job.baseSeconds ?? 2);\n  const maxInlineSeconds = Number(job.maxInlineSeconds ?? 300);\n  if (!Number.isInteger(attempt) || attempt < 1 ||\n      !Number.isInteger(maxAttempts) || maxAttempts < 1 ||\n      !Number.isFinite(baseSeconds) || baseSeconds < 1 ||\n      !Number.isFinite(maxInlineSeconds) || maxInlineSeconds < 1 ||\n      !Number.isFinite(nowMs)) throw new Error('Invalid retry configuration');\n  const result = (decision, reason, waitSeconds = 0, extra = {}) => ({\n    ...job, decision, reason, waitSeconds, ...extra,\n  });\n  if (status >= 200 && status < 300) return result('success', 'http_success');\n  if (status !== 429) return result('stop', 'not_a_429');\n  if (job.safeToRetry !== true) return result('review', 'repeat_not_confirmed_safe');\n  if (attempt >= maxAttempts) return result('stop', 'attempt_budget_exhausted');\n  const headers = Object.fromEntries(Object.entries(response.headers || {})\n    .map(([key, value]) => [key.toLowerCase(), value]));\n  const raw = headers['retry-after'];\n  let providerSeconds = null;\n  let source = 'fallback_backoff';\n  if (typeof raw === 'string' || typeof raw === 'number') {\n    const value = String(raw).trim();\n    if (/^\\d+$/.test(value)) {\n      providerSeconds = Number(value);\n      source = 'retry_after_seconds';\n    } else if (/^(?:[A-Za-z]{3},\\s*\\d{1,2}\\s+[A-Za-z]{3}\\s+\\d{4}|[A-Za-z]+,\\s*\\d{2}-[A-Za-z]{3}-\\d{2}|[A-Za-z]{3}\\s+[A-Za-z]{3}\\s+\\d{1,2}\\s)/.test(value)) {\n      const retryAt = Date.parse(value);\n      if (Number.isFinite(retryAt)) {\n        // Using the response Date as well is conservative when clocks differ.\n        const serverNow = Date.parse(String(headers.date || ''));\n        const reference = Number.isFinite(serverNow) ? Math.min(nowMs, serverNow) : nowMs;\n        providerSeconds = Math.max(0, Math.ceil((retryAt - reference) / 1000));\n        source = 'retry_after_http_date';\n      }\n    }\n  }\n  if (!Number.isFinite(providerSeconds)) providerSeconds = null;\n  const fallback = Math.min(60, baseSeconds * 2 ** Math.min(attempt - 1, 20));\n  const minimumSeconds = Math.max(1, Math.ceil(providerSeconds ?? fallback));\n  const jitterSeconds = Math.min(1, Math.max(0, Number(random()) || 0));\n  // Add jitter AFTER the provider floor; never multiply that floor down.\n  const waitSeconds = Math.ceil(minimumSeconds + jitterSeconds);\n  const notBefore = new Date(nowMs + waitSeconds * 1000).toISOString();\n  const extra = { minimumSeconds, source, notBefore, nextAttempt: attempt + 1 };\n  if (waitSeconds > maxInlineSeconds) return result('defer', 'wait_exceeds_inline_budget', waitSeconds, extra);\n  return result('retry', 'within_budget', waitSeconds, extra);\n}\nreturn $input.all().map(({json})=>({json:retryPolicy(json,Date.now(),()=>0)}));"
      },
      "position": [
        1220,
        280
      ]
    },
    {
      "id": "retry-allowed",
      "name": "Retry allowed",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "retry-check",
              "leftValue": "={{ $json.decision }}",
              "rightValue": "retry",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "position": [
        1440,
        280
      ]
    },
    {
      "id": "wait-provider-interval",
      "name": "Wait provider interval",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "parameters": {
        "amount": "={{ $json.waitSeconds }}",
        "unit": "seconds"
      },
      "position": [
        1660,
        160
      ]
    },
    {
      "id": "advance-attempt",
      "name": "Advance attempt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "return $input.all().map(({json:j})=>({json:{...j,attempt:j.nextAttempt}}));"
      },
      "position": [
        1880,
        160
      ]
    },
    {
      "id": "check-loop-outcome",
      "name": "Check loop outcome",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "const j=$input.first().json; const gap=(Date.parse(j.history[1]?.at)-Date.parse(j.history[0]?.at))/1000; if(j.decision!=='success'||j.attempt!==2||j.history.length!==2||gap<2)throw new Error('Loop demo failed'); return [{json:{result:'PASS',attempts:j.attempt,statuses:j.history.map(h=>h.statusCode),minimumWaitSeconds:2,observedGapSeconds:gap,jobId:j.request.jobId,note:'Simulated response data; real n8n Wait node; no external API called'}}];"
      },
      "position": [
        1660,
        440
      ]
    },
    {
      "id": "request-context",
      "name": "Request context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "return $input.all();"
      },
      "position": [
        560,
        360
      ]
    },
    {
      "id": "attach-response",
      "name": "Attach response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "return $input.all().map((item,i)=>{const j=$('Request context').itemMatching(i).json;return {json:{...j,response:item.json,history:[...j.history,{attempt:j.attempt,statusCode:item.json.statusCode,at:new Date().toISOString()}]},pairedItem:{item:i}};});"
      },
      "position": [
        1000,
        280
      ]
    }
  ],
  "connections": {
    "Run loop demo": {
      "main": [
        [
          {
            "node": "Prepare demo job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare demo job": {
      "main": [
        [
          {
            "node": "Request context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Simulate 429 then 200": {
      "main": [
        [
          {
            "node": "Attach response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decide next step": {
      "main": [
        [
          {
            "node": "Retry allowed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request context": {
      "main": [
        [
          {
            "node": "Simulate 429 then 200",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attach response": {
      "main": [
        [
          {
            "node": "Decide next step",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retry allowed": {
      "main": [
        [
          {
            "node": "Wait provider interval",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Check loop outcome",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait provider interval": {
      "main": [
        [
          {
            "node": "Advance attempt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Advance attempt": {
      "main": [
        [
          {
            "node": "Request context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "pinData": {}
}

Tested on October 7, 2026: We ran an 18-case response-policy lab and a timed retry loop in n8n Cloud. The responses were simulated; the Code, IF and Wait nodes ran in a real workspace. These tests do not measure a provider’s live capacity.

n8n rate limit retry flow: inspect response, check safety and attempts, wait or defer, then retry
An outgoing API retry flow. A delay belongs between attempts, and the job travels through the whole loop.

What an HTTP 429 error means in n8n

HTTP 429 means the server is rejecting requests because of a rate limit. In n8n, you may see “The service is receiving too many requests from you.” The limit belongs to the service you are calling; it is not one universal n8n requests-per-minute allowance. n8n’s rate-limit guide describes fixed retries, batching and waits.

Check the response body as well as the status. A short request burst, an exhausted account quota and a gateway restriction can need different fixes. If the provider says your paid quota is exhausted, a retry loop cannot buy more capacity.

Also separate two jobs: protecting an incoming webhook from too much traffic, and slowing outgoing requests to another API. An inbound limiter such as Upstash’s Redis-based n8n example solves the first problem. This article handles the second.

Choose a fixed retry, batching or a response-aware loop

Use the simplest method that matches the failure. You do not need a custom loop for every occasional 429.

On a phone, swipe the table sideways to read all columns.

ParameterBuilt-in retry or batchingThis response-aware demo
Delay sourceA configured retry delay or batch intervalRetry-After first; fallback backoff otherwise
UnitsHTTP batch interval and retry delay use millisecondsThe demo’s Wait node uses seconds
Attempt budgetConfigure the node’s retry settingsFour total attempts, including the first
Original jobDepends on your surrounding workflowExplicit request context restored after each response
Long provider waitDepends on configured settingsOver 300 seconds becomes a defer decision
Shared quotaLocal pacing alone is insufficientStill needs a shared limiter or coordinated worker

For preventive pacing, start with Items per Batch = 1 and a batch interval that fits the provider’s documented quota. For example, 1,000 milliseconds spaces batches roughly one second apart; it is an illustrative setting, not a guarantee that your API permits that pace. Configure these options in the HTTP Request node.

Use Retry On Fail for a modest fixed-delay retry when that fits the API. Use a custom loop when you need to read response headers, preserve a job, and make different decisions about retrying, stopping or deferring.

Download the free n8n Retry-After lab

Download the Retry-After lab ZIP. It contains two importable workflows, the JavaScript policy, 18 fixtures, test results and setup notes. No credentials or external API calls are required.

  • Policy tests: Check 18 synthetic responses without waiting.
  • Timed loop: Simulate a 429 with a two-second Retry-After, run a real Wait node, then simulate HTTP 200.

The workflows are teaching labs. They do not include a production API integration, a shared rate limiter or a durable queue for deferred jobs.

1. Import into a new blank workflow

Unzip the download. Create a new blank workflow in n8n and use Import → From file to select the policy-test JSON. Import the loop JSON into a separate blank workflow. Importing into an existing canvas can add nodes beside your current ones.

Both labs use a Manual Trigger. Keep them separate from live customer workflows while you learn how their decisions work.

2. Run the 18-case policy test

Click Execute workflow, then open Assert 18 results. The expected result is PASS, with 18 passed and zero failed. The fixed test clock and fixed jitter make the expected numbers reproducible.

Real n8n Cloud execution of the four-node Retry-After policy lab with all nodes completed
Actual policy-lab execution in n8n Cloud. Response fixtures are synthetic.

3. Run the timed retry loop

Execute the second workflow. The first simulated response is 429 with a Retry-After value of two seconds. The policy allows a retry, Wait pauses, the attempt counter advances, and the next simulated response is 200.

The final node checks the job ID, two attempts, the 429 → 200 sequence, and a gap of at least two seconds. A successful node execution alone would not prove those conditions; that is why the demo asserts them.

Real n8n Cloud execution of the response-aware retry loop using a Wait node and preserved request context
Actual timed-loop execution. This tests n8n control flow, not a live provider’s rate limit.

4. Read the decision before changing the demo

The policy returns one of five labels: success, retry, stop, review or defer. These are labels we defined in the demo, not built-in n8n execution statuses.

A 2xx response becomes success. A safe 429 below the attempt budget may retry. An unapproved write goes to review. Other error statuses and an exhausted budget stop. A wait above the demo’s inline limit becomes defer.

The demo’s false IF branch is an assertion for the expected successful run. In a real workflow, replace that assertion with explicit branches for success, stop, review and defer. Do not send all four to the same “completed” action.

How Retry-After changes the wait

Retry-After can contain a non-negative integer number of seconds or an HTTP date. Both formats are defined in RFC 9110, section 10.2.3. A parser that only converts the header to a number misses the date format.

Our policy accepts either form, rounds the wait up, and adds a small amount of positive jitter. Missing or invalid headers use fallback delays of two, four and eight seconds before jitter. The fallback can grow to a 60-second ceiling; a valid provider instruction is never shortened to that ceiling.

Keep this rule: A valid Retry-After value is a minimum wait, not a target to shrink. Multiplying a 30-second provider wait by a random fraction can send the next request too early. Add jitter after the minimum instead.

For date headers, the policy uses the earlier of its local clock and a valid response Date header. That is a conservative choice when the local clock is ahead. Keep clocks synchronized; parsing a header cannot remove every timing uncertainty.

Slack provides a concrete example: a 429 with Retry-After of 30 requires waiting before retrying the same API method in that workspace. The scope matters; it is not automatically a ban on every Slack request everywhere. See Slack’s rate-limit documentation.

Retry-After vs X-RateLimit-Reset: read the units first

HeaderMeaningExample and next step
Retry-After: 30Minimum delay in secondsWait at least 30 seconds before a safe retry.
Retry-After: HTTP dateA date after which the client may retryCalculate seconds until that date, round up and account for clock differences.
X-RateLimit-ResetProvider-specific reset hintGitHub uses UTC epoch seconds for its primary limit. Other APIs may use another format. Read that API’s documentation before parsing.
No usable retry headerNo valid server delay availableUse a capped fallback and stop when the attempt budget is exhausted.

RFC 9110 defines Retry-After. GitHub documents its reset timestamp. The demo deliberately does not interpret every X-RateLimit-Reset header as an epoch timestamp.

Copy the Code node policy and response adapter

Use JavaScript in Run Once for All Items mode. First keep a node named Request context before the HTTP node. Put this adapter after the HTTP node to attach its full response to the corresponding job:

return $input.all().map((item,i)=>{const j=$('Request context').itemMatching(i).json;return {json:{...j,response:item.json,history:[...j.history,{attempt:j.attempt,statusCode:item.json.statusCode,at:new Date().toISOString()}]},pairedItem:{item:i}};});

Then use the following code in Decide next step. It is the exact policy from the tested demo, including its zero-jitter test setting. It reads job.response.headers, preserves the attempt budget and returns a decision. It does not send a request or sleep by itself.

Show complete Retry-After Code node
function retryPolicy(job, nowMs = Date.now(), random = Math.random) {
  const response = job.response || {};
  const status = Number(response.statusCode);
  const attempt = Number(job.attempt);
  const maxAttempts = Number(job.maxAttempts ?? 4);
  const baseSeconds = Number(job.baseSeconds ?? 2);
  const maxInlineSeconds = Number(job.maxInlineSeconds ?? 300);
  if (!Number.isInteger(attempt) || attempt < 1 ||
      !Number.isInteger(maxAttempts) || maxAttempts < 1 ||
      !Number.isFinite(baseSeconds) || baseSeconds < 1 ||
      !Number.isFinite(maxInlineSeconds) || maxInlineSeconds < 1 ||
      !Number.isFinite(nowMs)) throw new Error('Invalid retry configuration');
  const result = (decision, reason, waitSeconds = 0, extra = {}) => ({
    ...job, decision, reason, waitSeconds, ...extra,
  });
  if (status >= 200 && status < 300) return result('success', 'http_success');
  if (status !== 429) return result('stop', 'not_a_429');
  if (job.safeToRetry !== true) return result('review', 'repeat_not_confirmed_safe');
  if (attempt >= maxAttempts) return result('stop', 'attempt_budget_exhausted');
  const headers = Object.fromEntries(Object.entries(response.headers || {})
    .map(([key, value]) => [key.toLowerCase(), value]));
  const raw = headers['retry-after'];
  let providerSeconds = null;
  let source = 'fallback_backoff';
  if (typeof raw === 'string' || typeof raw === 'number') {
    const value = String(raw).trim();
    if (/^\d+$/.test(value)) {
      providerSeconds = Number(value);
      source = 'retry_after_seconds';
    } else if (/^(?:[A-Za-z]{3},\s*\d{1,2}\s+[A-Za-z]{3}\s+\d{4}|[A-Za-z]+,\s*\d{2}-[A-Za-z]{3}-\d{2}|[A-Za-z]{3}\s+[A-Za-z]{3}\s+\d{1,2}\s)/.test(value)) {
      const retryAt = Date.parse(value);
      if (Number.isFinite(retryAt)) {
        // Using the response Date as well is conservative when clocks differ.
        const serverNow = Date.parse(String(headers.date || ''));
        const reference = Number.isFinite(serverNow) ? Math.min(nowMs, serverNow) : nowMs;
        providerSeconds = Math.max(0, Math.ceil((retryAt - reference) / 1000));
        source = 'retry_after_http_date';
      }
    }
  }
  if (!Number.isFinite(providerSeconds)) providerSeconds = null;
  const fallback = Math.min(60, baseSeconds * 2 ** Math.min(attempt - 1, 20));
  const minimumSeconds = Math.max(1, Math.ceil(providerSeconds ?? fallback));
  const jitterSeconds = Math.min(1, Math.max(0, Number(random()) || 0));
  // Add jitter AFTER the provider floor; never multiply that floor down.
  const waitSeconds = Math.ceil(minimumSeconds + jitterSeconds);
  const notBefore = new Date(nowMs + waitSeconds * 1000).toISOString();
  const extra = { minimumSeconds, source, notBefore, nextAttempt: attempt + 1 };
  if (waitSeconds > maxInlineSeconds) return result('defer', 'wait_exceeds_inline_budget', waitSeconds, extra);
  return result('retry', 'within_budget', waitSeconds, extra);
}
return $input.all().map(({json})=>({json:retryPolicy(json,Date.now(),()=>0)}));

For live traffic, replace the final () => 0 with Math.random if you want positive jitter. Keep the provider’s minimum wait. Connect only decision === "retry" to a Wait node set to Seconds and {{ $json.waitSeconds }}; advance the attempt counter before looping. Route success, stop, review and defer separately.

What the tests actually verified

The policy lab passed 18 deterministic cases. An additional local sweep checked 28 combinations of provider delay and jitter; none produced a wait below the accepted provider minimum. These are correctness checks, not speed or reliability benchmarks.

On a phone, swipe the table sideways to read all columns.

Test inputPolicy resultWhy it matters
429; Retry-After: 30Retry after 31 secondsPositive jitter preserves the provider floor
HTTP date 30 seconds aheadRetry after 31 secondsDate headers are handled
Missing or malformed header; attempt 1Retry after 3 secondsFallback works without a usable header
429 on attempt 4StopFour means total attempts, not four extra retries
Retry-After: 3600Defer for at least 3601 secondsA long provider wait is not clipped to five minutes
Write not approved for retryReviewA flag cannot make an unsafe request safe
401 or 503StopThis policy handles 429, not every error
200 on attempt 4SuccessA successful final attempt still counts

The one-second addition above comes from the fixture’s fixed jitter value and rounding. The timed demo uses zero jitter to test its two-second minimum. Full inputs and expected outputs are in the download.

Environment: n8n Cloud 2.43.0, Manual Trigger, JavaScript Code nodes, one job in the timed loop, no credentials, no external requests. The final response-adapter run observed a 2.038-second gap and preserved the original job ID.

Adapt the loop to a real HTTP Request node

The HTTP adaptation below follows the documented node behavior. We tested the response-shaped data and job-linking pattern in the offline loop; we did not test your API’s credentials, response format or side effects.

1. Start with a safe, single-job request

Choose a read request first. Store its job ID, request parameters, attempt of one, and maximum attempts of four before the request. The demo sets safeToRetry to true for its fictional GET; decide that flag from your real operation.

For a write, use the provider’s documented idempotency support or a reliable deduplication check before allowing retries. Retain the same idempotency key for the same logical operation. Setting a Boolean flag does not prevent duplicate payments, posts or records.

2. Return the HTTP status and headers

Replace Simulate 429 then 200 with your HTTP Request node. In Options → Response, enable Include Response Headers and Status and Never Error, then choose a response format appropriate to the endpoint.

Never Error lets an HTTP error response reach your decision node; it does not mean the business action succeeded. Inspect the returned statusCode, headers and body. Transport failures, such as a timeout without an HTTP response, need a separate error path. Keep native Retry On Fail off when this custom loop owns retries, so the two mechanisms do not multiply attempts.

3. Restore the request context after the response

The request’s response may replace the incoming JSON. Keep Request context before the HTTP node and Attach response after it, so the job and counter survive.

The included adapter traces each response to its input using n8n’s documented itemMatching method, then stores the response under a response field. See n8n’s previous-node reference. Check that mapping with your real node output before increasing the number of items or adding branches.

4. Configure the Wait and terminal branches

The Wait node uses After Time Interval, the returned waitSeconds value, and the unit Seconds. Advance the attempt only on the retry branch, then return to Request context.

For defer, persist the job and its notBefore time in durable storage and arrange a later dispatcher. The demo does not build that dispatcher. For stop or review, record the job and reason and send an alert through your n8n error-handling setup. Treating a defer decision as an immediate retry defeats the provider’s instruction.

Why you can still get 429 after adding Wait

A Wait node slows one path. It does not reserve capacity across all executions using the same account.

  • Several workflows share a quota. Two individually paced workflows can exceed one account limit together. Use a shared limiter or a coordinated worker for that quota.
  • Your batch is still too large. A pause between batches does not undo a burst of requests inside a batch.
  • The limit uses another unit. Some services limit tokens, writes, workspace activity or endpoint usage as well as requests.
  • The payload is repeatedly rejected. An auth error or invalid request needs a fix, not more 429 retries.
  • The loop restarts successful work. Keep progress per operation. Our WordPress social automation guide explains why retrying a failed channel should preserve successful channels.

n8n’s API rate-limiting overview explains the distinction between workflow execution controls and API traffic controls. More workflow concurrency can increase pressure on a provider rather than solve its quota.

Remove unnecessary requests before tuning delays. For example, cleaning and deduplicating lead emails before enrichment avoids paying for avoidable duplicate calls.

Who it is for

This lab is for builders who can inspect a response and want to understand a bounded retry loop before connecting a real API. It is especially useful when an endpoint sends Retry-After and a fixed delay is too crude.

Who should avoid it

Avoid using the demo unchanged for financial writes, bulk publishing or high-volume production traffic. It has no provider-specific idempotency integration, durable deferred-job dispatcher or account-wide quota coordination. If fixed batching solves your occasional burst, keep that simpler setup.

Frequently asked questions

Does n8n have a rate limit node?

You can handle many outgoing limits with HTTP Request batching or Loop Over Items plus Wait. A Wait node alone is not an account-wide limiter; shared quotas need coordination across executions. This lab adds a response-aware policy rather than claiming one node solves every limit.

Can n8n Retry On Fail read Retry-After automatically?

The documented Retry On Fail setup uses a configured Wait Between Tries delay. If you need a delay calculated from the response header, use a response-aware path and verify it with your endpoint. Do not assume your fixed setting matches a provider’s changing wait.

Why does my n8n workflow keep getting 429 after a delay?

The delay may be shorter than the provider’s instruction, or other executions may consume the same quota. Check the response body, quota scope, batch size and traffic from other workflows. An exhausted account quota needs a capacity or usage change.

Can I retry a POST after HTTP 429?

Retry a write only when you have established that repeating it is safe for that provider and operation. Use documented idempotency keys or a reliable duplicate check, and keep the same logical job identity. A 429 handler by itself does not protect against duplicate side effects.

Next build: Browse our practical n8n workflow guides, or join the OperantSolo newsletter for tested examples and troubleshooting notes.

If you need a hosted workspace, you can try n8n Cloud through our partner link. We may earn a commission at no extra cost to you. The lab also uses ordinary n8n nodes; choosing a paid workspace does not raise a third-party API’s quota.

Scroll to Top

Discover more from OperantSolo

Subscribe now to keep reading and get access to the full archive.

Continue reading