AutomationFlowsSlack & Telegram › Handle API Retries with Exponential Backoff, Jitter, Slack and Email Alerts

Handle API Retries with Exponential Backoff, Jitter, Slack and Email Alerts

ByVenkata V @vvrr22042026 on n8n.io

This workflow is a reusable retry and resilience pattern for n8n.

Event trigger★★★★☆ complexity18 nodesExecute Workflow TriggerHTTP RequestSlackEmail SendStop And Error
Slack & Telegram Trigger: Event Nodes: 18 Complexity: ★★★★☆ Added:
Handle API Retries with Exponential Backoff, Jitter, Slack and Email Alerts — n8n workflow card showing Execute Workflow Trigger, HTTP Request, Slack integration

This workflow corresponds to n8n.io template #15459 — we link there as the canonical source.

This workflow follows the Emailsend → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "yCrYxATDJ17upNhY",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Reusable Retry Handler - Exponential Backoff, Jitter, Notifications, Suspend",
  "tags": [
    {
      "id": "tM2BVCBK8TQHwIyC",
      "name": "resilience",
      "createdAt": "2026-04-28T01:22:33.852Z",
      "updatedAt": "2026-04-28T01:22:33.852Z"
    },
    {
      "id": "5RJ1TQycQr3pHG5c",
      "name": "error-handling",
      "createdAt": "2026-04-28T00:02:06.158Z",
      "updatedAt": "2026-04-28T00:02:06.158Z"
    },
    {
      "id": "47RJUhI8wCGtedIg",
      "name": "retry",
      "createdAt": "2026-04-28T01:22:33.814Z",
      "updatedAt": "2026-04-28T01:22:33.814Z"
    }
  ],
  "nodes": [
    {
      "id": "c0345ede-b82e-488e-9686-d6426fa83654",
      "name": "Sticky Note - Retry Policy",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -992,
        -160
      ],
      "parameters": {
        "width": 856,
        "height": 1834,
        "content": "# Reusable Retry Handler with Exponential Backoff, Jitter, Slack/Email Alerts, and Manual Suspend\n\n## How it works\n\nThis workflow is a reusable retry and resilience pattern for n8n.\n\nIt can be called from any workflow using the Execute Workflow node. It runs a transient operation, classifies the result, retries retryable failures using exponential backoff with jitter, and stops retrying after a configurable maximum number of attempts.\n\nAt a high level, it:\n\n\u2022 Accepts a generic operation input from another workflow\n\u2022 Runs an HTTP/API operation that can be replaced with any app node\n\u2022 Retries only transient failures such as 408, 409, 425, 429, 500, 502, 503, and 504\n\u2022 Avoids retrying common permanent errors such as 400, 401, 403, 404, and 422\n\u2022 Calculates exponential backoff delay with random jitter to reduce retry storms\n\u2022 Sends a Slack message when all retries are completed\n\u2022 Sends an email notification when all retries are completed\n\u2022 Suspends the workflow at a Wait node for manual review\n\u2022 Optionally continues to Stop and Error so your global Error Workflow can capture the final failure\n\nThis template is useful for production workflows that call third-party APIs, SaaS apps, databases, or internal services that may fail temporarily.\n\n## Set up steps\n\nSetup should take around 10\u201315 minutes.\n\n\u2022 Import the workflow JSON into n8n\n\u2022 Configure Slack credentials in the Slack alert node\n\u2022 Configure SMTP credentials in the email alert node\n\u2022 Replace the demo HTTP Request node with your real API/app operation if needed\n\u2022 Set your default max attempts, base delay, max delay, jitter percentage, Slack channel, and email recipient\n\u2022 Call this workflow from other workflows using Execute Workflow\n\u2022 Use n8n credentials or environment variables for secrets; do not hardcode API keys\n\nThis template implements a generic retry wrapper for n8n workflows:\n\n- Exponential backoff\n- Random jitter\n- Max attempts\n- Retryable/non-retryable classification\n- Slack alert when retries are exhausted\n- Email alert when retries are exhausted\n- Suspend/wait for manual review after retries are exhausted\n- Optional final Stop and Error so a global Error Workflow can capture the failed execution\n\n## Default retry policy\n\nRetryable by default: 408, 409, 425, 429, 500, 502, 503, 504.\n\nNot retried by default: 400, 401, 403, 404, 422.\n\n## Backoff formula\n\n```text\nwaitSeconds = min(maxDelaySeconds, baseDelaySeconds * 2^(attempt - 1))\nfinalWait = waitSeconds +/- random(jitterPercent)\n```\n\n## How to use\n\n1. Import the template into n8n.\n2. Configure Slack credentials in the Slack node.\n3. Configure SMTP credentials in the Email node.\n4. Replace the demo HTTP Request node with your real operation, or pass HTTP inputs into the template.\n5. From another workflow, call this workflow using Execute Workflow.\n\n## Example input\n\n```json\n{\n  \"operationName\": \"Create customer in CRM\",\n  \"url\": \"https://api.example.com/customers\",\n  \"method\": \"POST\",\n  \"headers\": {\n    \"Authorization\": \"Bearer {{$env.API_TOKEN}}\"\n  },\n  \"body\": {\n    \"name\": \"Acme\"\n  },\n  \"maxAttempts\": 5,\n  \"baseDelaySeconds\": 30,\n  \"maxDelaySeconds\": 900,\n  \"jitterPercent\": 25,\n  \"notifyEmail\": \"ops@example.com\",\n  \"slackChannel\": \"#automation-alerts\"\n}\n```\n\n## Suspended workflow behavior\n\nAfter all retries are completed, the workflow sends Slack and email notifications, then pauses at a Wait node. This keeps the failed execution suspended for manual review. After review, resume the workflow using the Wait node resume URL. It then reaches Stop and Error, allowing your global error workflow to capture the final failure.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "99351134-0d4c-40e9-a1bd-6b22570caca9",
      "name": "Execute Workflow Trigger",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "notes": "Sub-workflow entry point. Call this retry wrapper from any workflow.",
      "position": [
        32,
        784
      ],
      "parameters": {},
      "notesInFlow": true,
      "typeVersion": 1
    },
    {
      "id": "0a43ab16-0b04-4cda-8c2a-07fac5079431",
      "name": "Normalize Input + Defaults",
      "type": "n8n-nodes-base.code",
      "notes": "Sets defaults for retry/backoff, notification routing, and correlation ID.",
      "position": [
        272,
        784
      ],
      "parameters": {
        "jsCode": "const input = $json;\nconst cfg = {\n  operationName: input.operationName || 'Generic transient operation',\n  url: input.url,\n  method: (input.method || 'GET').toUpperCase(),\n  headers: input.headers || {},\n  query: input.query || {},\n  body: input.body ?? {},\n  maxAttempts: Number(input.maxAttempts || 5),\n  baseDelaySeconds: Number(input.baseDelaySeconds || 30),\n  maxDelaySeconds: Number(input.maxDelaySeconds || 900),\n  jitterPercent: Number(input.jitterPercent ?? 25),\n  notifyEmail: input.notifyEmail || '',\n  slackChannel: input.slackChannel || '#automation-alerts',\n  correlationId: input.correlationId || `${Date.now()}-${Math.random().toString(16).slice(2)}`,\n  attempt: Number(input.attempt || 1),\n  startedAt: input.startedAt || new Date().toISOString(),\n  retryableStatusCodes: input.retryableStatusCodes || [408,409,425,429,500,502,503,504],\n  workflowName: $workflow.name,\n  executionId: $execution.id,\n};\nif (!cfg.url) throw new Error('Missing required input: url');\nreturn [{ json: cfg }];"
      },
      "notesInFlow": true,
      "retryOnFail": false,
      "typeVersion": 2
    },
    {
      "id": "17116af5-c245-448e-a846-756fdbe28135",
      "name": "Do Operation - Replace With Your Node",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Demo operation. Replace with your actual HTTP/API/app node. Never hardcode secrets.",
      "onError": "continueRegularOutput",
      "position": [
        576,
        704
      ],
      "parameters": {
        "url": "={{$json.url}}",
        "method": "={{$json.method}}",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "neverError": true,
              "fullResponse": true
            }
          }
        },
        "jsonBody": "={{JSON.stringify($json.body || {})}}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "x-correlation-id",
              "value": "={{$json.correlationId}}"
            }
          ]
        }
      },
      "notesInFlow": true,
      "typeVersion": 4.2
    },
    {
      "id": "26f802d8-b969-4924-b4b6-543d2550477b",
      "name": "Classify Result + Calculate Backoff",
      "type": "n8n-nodes-base.code",
      "notes": "Classifies result and calculates exponential backoff with jitter.",
      "position": [
        848,
        800
      ],
      "parameters": {
        "jsCode": "let cfg;\n\ntry {\n  const nextItems = $items('Prepare Next Attempt');\n\n  if (nextItems && nextItems.length > 0 && nextItems[0]?.json) {\n    cfg = nextItems[0].json;\n  } else {\n    cfg = $items('Normalize Input + Defaults')[0].json;\n  }\n} catch (e) {\n  cfg = $items('Normalize Input + Defaults')[0].json;\n}\n\nconst response = $json;\nconst statusCode = Number(response.statusCode || response.status || 0);\nconst ok = statusCode >= 200 && statusCode < 300;\nlet errorMessage = '';\nif (!ok) {\n  errorMessage = response.statusMessage || response.message || response.error?.message || `HTTP ${statusCode || 'unknown error'}`;\n}\nconst retryable = !ok && cfg.retryableStatusCodes.includes(statusCode);\nconst attemptsRemaining = cfg.attempt < cfg.maxAttempts;\nconst shouldRetry = retryable && attemptsRemaining;\nconst rawDelay = Math.min(cfg.maxDelaySeconds, cfg.baseDelaySeconds * Math.pow(2, cfg.attempt - 1));\nconst jitterRange = rawDelay * (cfg.jitterPercent / 100);\nconst jitter = Math.round((Math.random() * 2 - 1) * jitterRange);\nconst delaySeconds = Math.max(1, Math.round(rawDelay + jitter));\nreturn [{ json: { ...cfg, statusCode, success: ok, retryable, shouldRetry, attemptsRemaining, delaySeconds, nextAttempt: cfg.attempt + 1, errorMessage, responseBody: response.body ?? response, lastAttemptAt: new Date().toISOString() }}];"
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "92f16e5d-395c-464f-8ea8-5b5b2acef35b",
      "name": "Success?",
      "type": "n8n-nodes-base.if",
      "position": [
        1104,
        784
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "36755111-c95b-4a50-9a40-ba1aa95793eb",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{$json.success}}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "c6c19f84-dad8-4e3b-857a-8218b0822e89",
      "name": "Retryable and Attempts Left?",
      "type": "n8n-nodes-base.if",
      "position": [
        1376,
        704
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "54728007-5ced-450c-b572-f4bbb97aa98e",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{$json.shouldRetry}}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "ef619caf-1a62-44e4-a2c3-98d76587b2ef",
      "name": "Wait - Exponential Backoff Delay",
      "type": "n8n-nodes-base.wait",
      "notes": "Suspends this execution for the calculated delay, then resumes with the same data.",
      "position": [
        1536,
        816
      ],
      "parameters": {
        "amount": "={{ $json.delaySeconds}}"
      },
      "notesInFlow": true,
      "typeVersion": 1.1
    },
    {
      "id": "69a0b607-715a-481d-8d76-a924984e7c3d",
      "name": "Prepare Next Attempt",
      "type": "n8n-nodes-base.code",
      "position": [
        1728,
        912
      ],
      "parameters": {
        "jsCode": "return [{ json: { ...$json, attempt: $json.nextAttempt, previousAttempts: [ ...($json.previousAttempts || []), { attempt: $json.attempt, statusCode: $json.statusCode, errorMessage: $json.errorMessage, delaySeconds: $json.delaySeconds, at: $json.lastAttemptAt } ] }}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ed7f90a3-b6d8-4c75-a6c7-071f337772a7",
      "name": "Return Success Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        1392,
        528
      ],
      "parameters": {
        "jsCode": "return [{ json: { outcome: 'success', operationName: $json.operationName, correlationId: $json.correlationId, attemptsUsed: $json.attempt, statusCode: $json.statusCode, responseBody: $json.responseBody, completedAt: new Date().toISOString() }}];"
      },
      "typeVersion": 2
    },
    {
      "id": "9081af81-5469-4819-adc7-d810bada8337",
      "name": "Build Final Failure Summary",
      "type": "n8n-nodes-base.code",
      "notes": "Creates the final failure payload after all retries are completed.",
      "position": [
        2048,
        704
      ],
      "parameters": {
        "jsCode": "function redact(obj) {\n  const blocked = ['authorization','api_key','apikey','token','password','secret','client_secret'];\n  if (obj === null || obj === undefined) return obj;\n  if (Array.isArray(obj)) return obj.map(redact);\n  if (typeof obj === 'object') {\n    return Object.fromEntries(Object.entries(obj).map(([k,v]) => {\n      if (blocked.some(b => k.toLowerCase().includes(b))) return [k, '***REDACTED***'];\n      return [k, redact(v)];\n    }));\n  }\n  return obj;\n}\nconst failure = {\n  outcome: 'retry_exhausted',\n  severity: 'HIGH',\n  operationName: $json.operationName,\n  correlationId: $json.correlationId,\n  executionId: $json.executionId,\n  workflowName: $json.workflowName,\n  attemptsUsed: $json.attempt,\n  maxAttempts: $json.maxAttempts,\n  statusCode: $json.statusCode,\n  errorMessage: $json.errorMessage,\n  retryable: $json.retryable,\n  startedAt: $json.startedAt,\n  failedAt: new Date().toISOString(),\n  lastResponse: redact($json.responseBody),\n  input: redact({ url: $json.url, method: $json.method, headers: $json.headers, query: $json.query, body: $json.body }),\n  previousAttempts: $json.previousAttempts || [],\n  slackChannel: $json.slackChannel,\n  notifyEmail: $json.notifyEmail\n};\nreturn [{ json: failure }];"
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "5590c3c8-70fd-46f4-9135-70dbc73b85e3",
      "name": "Send Slack Alert",
      "type": "n8n-nodes-base.slack",
      "notes": "Configure Slack credentials and channel after import.",
      "position": [
        2240,
        560
      ],
      "parameters": {
        "text": "=\ud83d\udea8 *n8n retries exhausted*\\n*Operation:* {{$json.operationName}}\\n*Workflow:* {{$json.workflowName}}\\n*Attempts:* {{$json.attemptsUsed}}/{{$json.maxAttempts}}\\n*Status:* {{$json.statusCode}}\\n*Error:* {{$json.errorMessage}}\\n*Correlation ID:* {{$json.correlationId}}\\n*Execution:* {{$json.executionId}}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#n8n"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "typeVersion": 2.3
    },
    {
      "id": "6f5de589-deee-45c5-955c-17db2bc35f1f",
      "name": "Send Email Alert",
      "type": "n8n-nodes-base.emailSend",
      "notes": "Configure SMTP credentials after import.",
      "position": [
        2304,
        800
      ],
      "parameters": {
        "html": "=Click to resume workflow {{$execution.resumeUrl}}",
        "options": {},
        "subject": "=n8n retries exhausted: {{$json.operationName}}\n\n{{$execution.resumeUrl}}",
        "toEmail": "=vr.aus12@gmail.com",
        "fromEmail": "user@example.com"
      },
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "typeVersion": 2.1
    },
    {
      "id": "dc81b662-a28c-40a4-a06f-9c646afcc93e",
      "name": "Suspend - Wait for Manual Review",
      "type": "n8n-nodes-base.wait",
      "notes": "This suspends the execution after notifications are sent. Use the generated resume URL to continue after manual review.",
      "position": [
        2608,
        624
      ],
      "parameters": {
        "resume": "webhook",
        "options": {}
      },
      "notesInFlow": true,
      "typeVersion": 1.1
    },
    {
      "id": "97a2f47d-0ab3-42e6-af43-1c8ae83b44e7",
      "name": "Stop and Error After Manual Review",
      "type": "n8n-nodes-base.stopAndError",
      "notes": "Forces a final failure so your global Error Workflow can capture this if required.",
      "position": [
        2624,
        832
      ],
      "parameters": {
        "errorMessage": "=Retries exhausted for {{$json.operationName}} after {{$json.maxAttempts}} attempts. Correlation ID: {{$json.correlationId}}"
      },
      "notesInFlow": true,
      "typeVersion": 1
    },
    {
      "id": "ebb0ad2b-972f-41d6-a245-979e1b0cca64",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -64,
        624
      ],
      "parameters": {
        "color": 7,
        "width": 768,
        "height": 416,
        "content": "## Workflow tigger and API call"
      },
      "typeVersion": 1
    },
    {
      "id": "55dc6663-c9cd-4ce9-b945-c3d61dc937e5",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        752,
        464
      ],
      "parameters": {
        "color": 7,
        "width": 1184,
        "height": 768,
        "content": "## Retry check"
      },
      "typeVersion": 1
    },
    {
      "id": "a421aba9-5dc7-41fe-a8c4-77ba73573043",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1984,
        464
      ],
      "parameters": {
        "color": 7,
        "width": 1552,
        "height": 864,
        "content": "## Alert and suspend workflow on retries exhaution"
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "6e90a3bb-1bd2-4f41-bb4b-e4f35a4561fd",
  "connections": {
    "Success?": {
      "main": [
        [
          {
            "node": "Return Success Payload",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Retryable and Attempts Left?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Email Alert": {
      "main": [
        [
          {
            "node": "Suspend - Wait for Manual Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Slack Alert": {
      "main": [
        [
          {
            "node": "Send Email Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Next Attempt": {
      "main": [
        [
          {
            "node": "Do Operation - Replace With Your Node",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute Workflow Trigger": {
      "main": [
        [
          {
            "node": "Normalize Input + Defaults",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Input + Defaults": {
      "main": [
        [
          {
            "node": "Do Operation - Replace With Your Node",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Final Failure Summary": {
      "main": [
        [
          {
            "node": "Send Slack Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retryable and Attempts Left?": {
      "main": [
        [
          {
            "node": "Wait - Exponential Backoff Delay",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Final Failure Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Suspend - Wait for Manual Review": {
      "main": [
        [
          {
            "node": "Stop and Error After Manual Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait - Exponential Backoff Delay": {
      "main": [
        [
          {
            "node": "Prepare Next Attempt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Result + Calculate Backoff": {
      "main": [
        [
          {
            "node": "Success?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Do Operation - Replace With Your Node": {
      "main": [
        [
          {
            "node": "Classify Result + Calculate Backoff",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow is a reusable retry and resilience pattern for n8n.

Source: https://n8n.io/workflows/15459/ — original creator credit. Request a take-down →

More Slack & Telegram workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Slack & Telegram

Wait Slack. Uses httpRequest, xml, splitInBatches, stickyNote. Event-driven trigger; 28 nodes.

HTTP Request, XML, Execute Workflow Trigger +1
Slack & Telegram

Trigger: Launched by a parent workflow through a Slack shortcut with modal input. API Integration: Utilizes the Qualys API for vulnerability scanning. Data Conversion: Converts XML scan results to JSO

HTTP Request, XML, Execute Workflow Trigger +1
Slack & Telegram

Back Up Your N8N Workflows To Github. Uses manualTrigger, stickyNote, executeWorkflowTrigger, n8n. Event-driven trigger; 26 nodes.

Execute Workflow Trigger, n8n, HTTP Request +2
Slack & Telegram

This workflow will backup your workflows to Github. It uses the public api to export all of the workflow data using the n8n node.

Execute Workflow Trigger, n8n, HTTP Request +2
Slack & Telegram

Wait Slack. Uses httpRequest, xml, splitInBatches, slack. Event-driven trigger; 22 nodes.

HTTP Request, XML, Slack +1