Fix n8n's MCP Webhook Heap Leak in Queue Mode

Why memory climbs to an OOM crash on n8n workers running an MCP Trigger in queue mode, and the exact version that fixes the SessionManager leak.

Title card: Fix n8n's MCP Webhook Heap Leak in Queue Mode
The queue-mode MCP SessionManager leak and the n8n release that closes it.

TL;DR: In n8n queue mode, the MCP Trigger's SessionManager never frees Streamable HTTP sessions when a client skips the HTTP DELETE, so heap leaks ~5 MB per session until the worker OOMs - upgrade to n8n 2.30.0.

This one only bites a specific setup: n8n running in queue mode with an MCP Trigger exposed over Streamable HTTP, hit by a stateless client such as AWS Bedrock Agents. Memory on the worker climbs steadily across hours or days with no matching increase in traffic, then the process dies. Most "n8n out of memory" guides point you at raising the heap limit or trimming execution data, which does nothing here because the leak is in session cleanup, not payload size. The root cause was confirmed and fixed upstream in n8n issue #32889.

What causes the n8n MCP webhook heap leak in queue mode?

The MCP Trigger keeps one live session object per connected client so it can route follow-up requests back to the right transport. Sessions are supposed to be torn down when the client goes away. n8n has two cleanup paths, and they do not cover the same cases:

  • SSE transport: cleanup is wired to resp.on('close'), which fires whenever the underlying HTTP response socket closes, so a dropped connection reliably frees the session.
  • Streamable HTTP transport: cleanup is wired only to transport.onclose, which fires when the client sends an explicit HTTP DELETE to end the session.

That gap is the whole bug. A client that treats each MCP call as stateless never sends the DELETE. AWS Bedrock Agents behave exactly this way, and so do some serverless HTTP callers. The session object is never released, the InMemorySessionStore keeps growing, and each retained entry holds roughly 5 MB. At 329 abandoned sessions you are already sitting on about 1.6 GB of dead objects. In queue mode the leak lands on the worker process that handles the trigger, so a busy worker crashes first while the main instance looks healthy, which is why the cause is easy to misread.

Comparison of session cleanup in n8n: SSE sessions are freed by resp.on('close') on socket close, while Streamable HTTP sessions rely on transport.onclose which needs an HTTP DELETE the stateless client never sends, so the session is retained and heap grows about 5 MB each.
SSE cleanup fires on socket close; Streamable HTTP cleanup waits for a DELETE that stateless clients like Bedrock Agents never send.

How do you confirm it is the SessionManager leak and not something else?

Three signals together are near-conclusive:

  • The crash string is exact. The worker exits with FATAL ERROR: Ineffective mark-compacts near heap limit / Allocation failed - JavaScript heap out of memory. That is V8 giving up, not n8n throwing a workflow error.
  • Heap tracks connection count, not execution volume. Plot resident memory over time: it rises in steps that line up with new MCP clients connecting, and it never comes back down even when workflow throughput is flat or idle.
  • The client is a Streamable HTTP caller that does not send DELETE. If your MCP consumer is Bedrock Agents, a Lambda, or any stateless HTTP integration, you match the trigger profile. Pure SSE clients do not.

If memory instead spikes with large single executions and recovers afterwards, you are looking at execution-data bloat, not this leak, and the fix is different. This is the same discipline that separates one failure mode from another across the Automation Error Index of n8n errors: match the exact symptom before you change anything.

Stepped line of worker heap rising as retained MCP sessions accumulate at roughly 5 MB each: about 0.5 GB at 100 sessions, 1.6 GB at 329 sessions, reaching the ~2 GB Node heap ceiling where the worker crashes.
Heap climbs with connection count, not execution volume, until it hits the Node ceiling and the worker crashes.

How do you fix the n8n MCP webhook heap leak?

The supported fix is a version upgrade. n8n 2.30.0, released 2026-07-07, adds a resp.on('close') fallback alongside the existing transport.onclose handler in the Streamable HTTP setup, so a session is freed when the socket closes even if the client never sends DELETE.

# pin the fixed version and restart the whole queue-mode fleet
# (main + every worker must run the same version)
docker compose pull
docker compose up -d
docker compose exec n8n n8n --version   # expect 2.30.0 or newer

Upgrade every node in the deployment, not just the main instance. The leak lives on whichever process runs the MCP Trigger, and in queue mode that is the workers. Restarting workers on the old version only resets the clock; heap starts climbing again on the next batch of stateless clients. If you are still setting up your cluster, the reverse-proxy and multi-worker layout in our guide to self-hosting n8n with Docker Compose is the same topology this fix applies to.

How do you patch it if you cannot upgrade past 2.29?

If you are pinned below 2.30 for compatibility reasons, you can backport the one-line fix. The leak is in handleStreamableHttpSetup; add the socket-close fallback so cleanup runs on either event:

// packages/@n8n/nodes-langchain/.../McpServer.ts
// inside handleStreamableHttpSetup, after the transport is created:
transport.onclose = () => this.cleanupSession(sessionId);

// add this fallback so a dropped socket also frees the session:
resp.on('close', () => this.cleanupSession(sessionId));

Make cleanupSession idempotent (guard against a double-free when both events fire on a clean shutdown) and rebuild the image. This is a stopgap: track the upstream SessionManager fix in issue #32889 and drop the patch once you can move to 2.30.0. A safer interim mitigation while you schedule the upgrade is to restart workers on a fixed timer so heap never reaches the ceiling, but treat that as a bandage, not a cure.

Which n8n versions are affected?

Version rangeMCP queue-mode Streamable HTTP cleanupAction
2.26.8 - 2.29.xLeaks: only transport.onclose wiredUpgrade or patch
2.30.0 and newerFixed: resp.on('close') fallback addedUpgrade to this

The bug predates the 2.26.8 report in practice; any 2.x line with the Streamable HTTP transport and no socket-close fallback carries it. Treat 2.30.0 as the floor for any queue-mode deployment that exposes an MCP Trigger to external agents.

How do you verify the leak is gone after the fix?

  1. Confirm every process reports 2.30.0 or newer with n8n --version.
  2. Connect a stateless Streamable HTTP client (or replay Bedrock Agent traffic) and let it disconnect without a DELETE.
  3. Watch worker RSS for an hour: it should plateau, not step upward, once idle.
  4. Grep the worker logs for Ineffective mark-compacts over a full day; a clean run has none.

FAQ

Does raising NODE_OPTIONS max-old-space-size fix the n8n MCP heap leak?

No. A larger heap only delays the crash. The session objects are never released, so more memory just means more retained sessions before "Ineffective mark-compacts near heap limit" fires. Fix the cleanup path instead.

Why does only one worker crash and not the main n8n instance?

In queue mode the MCP Trigger runs on the worker that picks up the job, so the leak accumulates there. The main instance handles the editor and API and never holds the abandoned sessions, so it looks healthy while a worker dies.

What does "No transport found for session" mean in the same setup?

It means a follow-up request arrived for a session that was already cleaned up or never registered on that process. In queue mode it usually points to requests hitting a different worker than the one that opened the session; it is a routing/affinity issue, separate from the heap leak.

Do SSE-based MCP clients hit this leak?

No. SSE cleanup is wired to resp.on('close'), which fires on socket close, so SSE sessions are freed reliably. The leak is specific to Streamable HTTP clients that never send an HTTP DELETE.

Which n8n version should I pin to avoid it?

Pin 2.30.0 or newer across the whole fleet. That is the release that adds the resp.on('close') fallback to the Streamable HTTP transport.