Why n8n Webhooks Silently Drop Requests Under Load (And How to Fix Queue Backup)

Mike Holownych
#n8n#automation
Share:

Quick Answer

n8n’s default execution mode processes webhook-triggered workflows with a limited number of concurrent slots. Once webhook volume outpaces that capacity, incoming requests queue up — and if a webhook sender’s own timeout expires before n8n gets to processing that request, the sender marks the delivery failed (and some senders won’t retry), even though n8n itself never logged an error. The fix is switching to n8n’s Redis-backed queue mode (EXECUTIONS_MODE=queue) instead of relying on default in-process concurrency, plus responding to webhooks fast and processing asynchronously.

The Failure Pattern: 200-Status Responses, Missing Data

A webhook endpoint can return 200 and n8n’s own execution log can show “success” for everything it actually processed, while the count of things that should have arrived is higher than what n8n received or completed. That gap isn’t a bug in n8n reporting false success — it’s requests that never got a timely response and were marked failed by the sender before n8n’s queue caught up to them, so they never show up in n8n’s history at all.

This is a capacity problem, not a logic problem: n8n’s default execution mode holds a fixed number of concurrent executions. When webhook triggers arrive faster than that pool clears, later requests wait — and a webhook sender that expects a response within its own timeout window (specific to that sender, not n8n) doesn’t wait indefinitely. If your response comes after that window, the sender has already marked it a failed delivery.

Why Sequential Processing Creates a Bottleneck

n8n’s default (non-queue) execution mode processes triggered workflows using a bounded pool of concurrent executions, configured via EXECUTIONS_CONCURRENT_MAX(or n8n’s current equivalent — check your installed version’s documentation, since exact variable names have changed across n8n releases). Once every slot is occupied, new triggers wait for one to free up.

Under a burst of concurrent requests, three related failure modes compound:

Timeout collision. If your workflow takes several seconds to complete and the webhook sender expects a response well before that, requests queued behind others can blow past the sender’s timeout purely from wait time — the workflow itself may complete successfully seconds later, but the sender has already recorded the delivery as failed.

Memory pressure from a large queue. In-memory queuing holds request payloads until they’re processed. A backlog of many queued executions, each holding its own payload in memory, adds up — and under sustained overload can contribute to instability, not just slow responses.

One slow execution blocking others behind it. A single execution stuck on a slow downstream call (a database timeout, a rate-limited API) occupies a concurrency slot the whole time it’s stuck, delaying every execution queued behind it even if those wouldn’t individually have been slow.

A More Resilient Queue Setup

The fix that scales past default in-process concurrency is n8n’s built-in queue mode, backed by Redis, plus responding to webhooks immediately rather than making the sender wait for full processing.

1. Enable Redis-backed queue mode.

QUEUE_BULL_REDIS_HOST=localhost
QUEUE_BULL_REDIS_PORT=6379
QUEUE_BULL_REDIS_DB=0
EXECUTIONS_MODE=queue

Queue mode decouples “webhook received” from “workflow processed” — Redis holds the job durably instead of everything living in in-process memory, and you can scale worker processes independently of the webhook-receiving process. See n8n’s own queue mode documentation for the full setup, including running separate worker processes.

2. Tune queue behavior for your workload.

{
  "queue": {
    "bull": {
      "settings": {
        "stalledInterval": 30000,
        "maxStalledCount": 1,
        "retryProcessDelay": 5000
      }
    }
  }
}

3. Add explicit retry handling for transient failures, rather than letting a single failed attempt drop the job silently:

if ($json.error) {
  return {
    retry: ($execution.mode === 'webhook' && $execution.retryCount < 3),
    delay: 5000 * Math.pow(2, $execution.retryCount || 0),
    error: $json.error,
  };
}
return $json;

4. Set sane per-workflow execution limits so one runaway workflow doesn’t consume shared capacity indefinitely:

{
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": false,
    "callerPolicy": "workflowsFromSameOwner",
    "executionTimeout": 300,
    "maxTimeout": 3600
  }
}

Add Visibility, Not Just Capacity

Queue mode fixes throughput, but you still want to know if drops are happening. A lightweight way to see queue health without external tooling is a Function node reporting on the queue’s own state:

const queueStats = {
  waiting: $('Webhook').getQueuedCount(),
  active: $('Webhook').getActiveCount(),
  completed: $('Webhook').getCompletedCount(),
  failed: $('Webhook').getFailedCount(),
  timestamp: new Date().toISOString(),
};

return { queueStats };

Compare n8n’s completed-execution count against the delivery count your webhook sender reports in its own dashboard (Stripe, Shopify, and most providers expose delivery logs) — a persistent gap between the two is the signal that something upstream of n8n’s own success logging is dropping requests.

Three Mistakes to Avoid

Raising concurrency instead of adopting queue mode. Bumping EXECUTIONS_CONCURRENT_MAX gives you more simultaneous executions, but each one still holds its payload and resources in the same process — it raises memory pressure rather than solving the underlying queuing problem. Redis-backed queue mode, with separate worker processes, scales more predictably than just raising the in-process ceiling.

Extending your own timeout instead of the sender’s. Setting a longer EXECUTIONS_TIMEOUT doesn’t change how long the webhook sender is willing to wait for a response — it only changes when n8n gives up on an execution that’s already missed the sender’s window. The fix for sender timeouts is responding fast and processing async, not a longer timeout on your side.

Disabling retries to avoid duplicate processing, instead of making your workflow idempotent. Turning off retries to prevent double-processing also means a transient failure (a momentary network blip, a brief rate limit) becomes a permanent drop instead of a recovered request. The more robust fix is keeping retries on and making the workflow itself idempotent — for example, keying database writes on the sender’s own idempotency/event ID so a retried delivery is a safe no-op rather than a duplicate.

MH

About Mike Holownych

Building AI Syndicate—governance infrastructure for AI agents in regulated environments. 20+ years enterprise operations, now applying that reliability discipline to AI deployment.