<figure class="wp-block-image aligncenter"><img src="https://flowgenius.in/wp-content/uploads/2026/01/n8n-queue-mode-memory-leak.png" alt="Step by Step Guide to solve n8n queue mode memory leak" /><figcaption style="text-align: center;">Step by Step Guide to solve n8n queue mode memory leak</p>
<hr />
</figcaption></figure>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Who this is for:</strong> Platform engineers and DevOps practitioners running n8n in production who need reliable, long‑running queue workers. <strong>We cover this in detail in the </strong><a href="https://flowgenius.in/n8n-queue-mode-error-guide/">n8n Queue Mode Errors Guide.</a></p>
<hr style="margin: 55px 0; border: none; height: 1px;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">Quick Diagnosis</h2>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Symptom:</strong> Queue workers crash after 30‑60 min with “JavaScript heap out of memory” or a SIGKILL.</p>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Quick fix</strong></p>
<ol style="margin-bottom: 2em; line-height: 1.9;">
<li>Increase the V8 heap: <code>NODE_OPTIONS="--max-old-space-size=4096"</code> in the worker container.</li>
<li>Enable the inspector (<code>--inspect</code>) and capture a heap snapshot with <code>heapdump</code>.</li>
<li>Pinpoint the offending node (usually a custom node or a huge JSON payload) and free its memory (<code>delete</code>, <code>clear</code>, or stream the data).</li>
<li>Add a health‑check that restarts any worker that runs longer than 45 min.</li>
</ol>
<hr style="margin: 55px 0; border: none; height: 1px;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">What’s happening?</h2>
<p><strong>If you encounter any </strong><a href="/n8n-queue-mode-logging-not-enabled">n8n queue mode logging not enabled </a><strong>resolve them before continuing with the setup.</strong></p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="padding: 12px 14px; text-align: left; border-bottom: 1px solid #e0e0e0;">Symptom</th>
<th style="padding: 12px 14px; text-align: left; border-bottom: 1px solid #e0e0e0;">Typical Log Message</th>
<th style="padding: 12px 14px; text-align: left; border-bottom: 1px solid #e0e0e0;">Immediate Impact</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Worker exits with code 137</td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">SIGKILL: Out of memory or JavaScript heap out of memory</td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Jobs stall; queue length spikes</td>
</tr>
<tr>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">CPU spikes to 100 % before crash</td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">node: internal/process/promises: …</td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Host becomes unresponsive</td>
</tr>
<tr>
<td style="padding: 12px 14px;">RSS grows linearly (500 MiB → 4 GiB)</td>
<td style="padding: 12px 14px;">No error until OOM killer fires</td>
<td style="padding: 12px 14px;">Crash is delayed, hard to reproduce</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Fast check</strong> – Run <code>docker stats</code> (or <code>kubectl top pod</code>) on the worker pods. A steady RSS climb without plateau signals a leak.</p>
<hr style="margin: 55px 0; border: none; height: 1px;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">1️⃣ Enable Precise Memory Monitoring in Workers</h2>
<p><strong>If you encounter any </strong><a href="/n8n-queue-mode-queue-retry-limit-exceeded">n8n queue mode queue retry limit exceeded </a><strong>resolve them before continuing with the setup.</strong></p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">1.1 Add Node‑level diagnostics (docker‑compose)</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em;"># docker‑compose.yml – worker service (environment)
services:
n8n-worker:
environment:
- NODE_OPTIONS=--max-old-space-size=4096 --inspect=0.0.0.0:9229
</pre>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em;"># docker‑compose.yml – worker service (ports)
ports:
- "9229:9229" # expose V8 inspector internally only
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><em>EEFA</em>: Bind the inspector to the internal network or tunnel via SSH; never expose it publicly.</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">1.2 Capture a heap snapshot on demand</h3>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Step 1 – Install <code>heapdump</code> in a custom node or pre‑execution hook</strong></p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em;">npm install heapdump --save</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Step 2 – Add a signal handler that writes a snapshot</strong></p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em;">const heapdump = require('heapdump');
process.on('SIGUSR2', () => {
const file = `/tmp/heap-${Date.now()}.heapsnapshot`;
heapdump.writeSnapshot(file, (err) => {
if (err) console.error(err);
else console.log('Heap snapshot written to', file);
});
});
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Step 3 – Trigger the snapshot when memory > 75 %</strong></p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em;">docker kill --signal=SIGUSR2 n8n-worker</pre>
<hr style="margin: 55px 0; border: none; height: 1px;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">2️⃣ Identify the Leak Source</h2>
<h3 style="margin-bottom: 45px; line-height: 1.3;">2.1 Analyze the snapshot</h3>
<ol style="margin-bottom: 2em; line-height: 1.9;">
<li>Open the <code>.heapsnapshot</code> in Chrome DevTools → <strong>Memory</strong> → <strong>Comparison</strong>.</li>
<li>Look for objects (e.g., <code>Array</code>, <code>Object</code>) retaining > 200 MiB.</li>
<li>Drill into the *constructor name* to see which node created the allocation (<code>LargeJsonNode</code>, <code>CsvParseNode</code>, etc.).</li>
</ol>
<h3 style="margin-bottom: 45px; line-height: 1.3;">2.2 Common culprits in n8n</h3>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="padding: 12px 14px; text-align: left; border-bottom: 1px solid #e0e0e0;">Node / Feature</th>
<th style="padding: 12px 14px; text-align: left; border-bottom: 1px solid #e0e0e0;">Typical Leak Pattern</th>
<th style="padding: 12px 14px; text-align: left; border-bottom: 1px solid #e0e0e0;">Fix Hint</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Custom JavaScript node</td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Large <code>item</code> arrays stored in a closure or global</td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Return only needed data; <code>delete</code> large fields after use</td>
</tr>
<tr>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">HTTP Request (big response)</td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Buffer kept in memory because <code>binary</code> isn’t cleared</td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Stream to a temp file and <code>delete binary</code> after processing</td>
</tr>
<tr>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Webhook with long‑running connections</td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Event listeners never removed on restart</td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Call <code>process.removeAllListeners()</code> in an <code>onClose</code> hook</td>
</tr>
<tr>
<td style="padding: 12px 14px;">CSV/JSON parser on huge payloads</td>
<td style="padding: 12px 14px;">Entire dataset held in a single array</td>
<td style="padding: 12px 14px;">Use <code>csv-parser</code> or <code>JSONStream</code> to process line‑by‑line</td>
</tr>
</tbody>
</table>
<h3 style="margin-bottom: 45px; line-height: 1.3;">2.3 Verify with a minimal reproduction workflow</h3>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Node 1 – Download a large JSON file</strong></p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em;">{
"type": "n8n-nodes-base.httpRequest",
"parameters": {
"url": "https://example.com/large-file.json",
"responseFormat": "json",
"options": { "jsonParse": false }
},
"name": "Download Large JSON"
}
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Node 2 – Pass‑through function (no processing)</strong></p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em;">{
"type": "n8n-nodes-base.function",
"parameters": {
"functionCode": "return items;"
},
"name": "Pass‑Through"
}
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">Connect the two nodes and run the workflow repeatedly (e.g., via a cron). If memory climbs, the leak originates from the HTTP request handling.</p>
<hr style="margin: 55px 0; border: none; height: 1px;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">3️⃣ Apply Production‑Grade Fixes</h2>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.1 Enforce memory limits and graceful restarts</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em;"># docker‑compose.yml – memory & restart policy
services:
n8n-worker:
mem_limit: 4g
restart: on-failure
</pre>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em;"># docker‑compose.yml – health‑check that forces a restart before OOM
healthcheck:
test: ["CMD", "node", "-e",
"process.exit(process.memoryUsage().heapUsed > 3.5e9 ? 1 : 0)"]
interval: 5m
retries: 2
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><em>EEFA</em>: The health check restarts the pod before the OS OOM killer intervenes, preserving queue continuity.</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.2 Stream large payloads instead of buffering</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em;">// Stream a CSV download to a temporary file
const fs = require('fs');
const https = require('https');
const tmp = require('tmp');
async function streamCsv(url) {
const tmpFile = tmp.fileSync({ postfix: '.csv' });
const fileStream = fs.createWriteStream(tmpFile.name);
return new Promise((resolve, reject) => {
https.get(url, (res) => {
res.pipe(fileStream);
res.on('end', () => {
fileStream.close();
resolve(tmpFile.name);
});
}).on('error', reject);
});
}
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">After processing, <strong>delete</strong> the temp file:</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em;">fs.unlinkSync(tmpFile.name);
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.3 Explicitly free large objects in custom nodes</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em;">// Example: Clean up after heavy computation
items = items.map(item => {
const result = heavyComputation(item.json);
delete item.json.largePayload; // free original data
return { json: result };
});
global.gc && global.gc(); // trigger GC if --expose-gc is set
return items;
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">Enable <code>--expose-gc</code> only in staging environments to avoid production overhead.</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.4 Isolate memory‑intensive jobs with a dedicated worker pool</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">Add a second pool that only runs “heavy” jobs:</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em;"># .env for heavy‑worker pool
QUEUE_MODE=memory
WORKER_CONCURRENCY=2
WORKER_LABELS=heavy
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">In the workflow, set Execute on Worker Label → heavy. This isolates leaks to a subset of pods that can be cycled more aggressively.</p>
<hr style="margin: 55px 0; border: none; height: 1px;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">4️⃣ Validation & Ongoing Monitoring</h2>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="padding: 12px 14px; text-align: left; border-bottom: 1px solid #e0e0e0;">Tool</th>
<th style="padding: 12px 14px; text-align: left; border-bottom: 1px solid #e0e0e0;">Metric</th>
<th style="padding: 12px 14px; text-align: left; border-bottom: 1px solid #e0e0e0;">Alert Threshold</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Prometheus (<code>node_memory_Active_bytes</code>)</td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Worker RSS</td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">> 3.5 GiB (when limit = 4 GiB)</td>
</tr>
<tr>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Grafana (Heap Used)</td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;"><code>process_resident_memory_bytes</code></td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">80 % of <code>max-old-space-size</code></td>
</tr>
<tr>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Sentry (error fingerprint)</td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">JavaScript heap out of memory</td>
<td style="padding: 12px 14px; border-bottom: 1px solid #e0e0e0;">Immediate ticket</td>
</tr>
<tr>
<td style="padding: 12px 14px;">Datadog (container restart count)</td>
<td style="padding: 12px 14px;"><code>container_restart_total</code></td>
<td style="padding: 12px 14px;">> 2 per hour</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;">Create a dashboard that shows **memory trend per worker** and **queue backlog**. Correlate spikes with workflow IDs (available via <code>process.env.N8N_WORKFLOW_ID</code> in logs).</p>
<hr style="margin: 55px 0; border: none; height: 1px;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">5️⃣ Preventive Best Practices (Checklist)</h2>
<ul style="margin-bottom: 2em; line-height: 1.9;">
<li><strong>Never store full payloads</strong> in <code>workflowData</code> or <code>global</code> longer than the node execution.</li>
<li><strong>Stream files > 10 MiB</strong>; use <code>/tmp</code> and delete after use.</li>
<li><strong>Limit concurrency</strong> (<code>WORKER_CONCURRENCY</code>) for jobs that parse large data sets.</li>
<li><strong>Pin Node.js to LTS</strong> (e.g., v20.x) to benefit from V8 memory‑leak fixes.</li>
<li><strong>Run <code>npm audit</code></strong> on custom node packages; upgrade any that depend on outdated <code>request</code> or <code>xml2js</code>.</li>
<li><strong>Enable health‑check auto‑restart</strong> (see §3.1).</li>
<li><strong>Document</strong> any custom node that allocates > 50 MiB and add a “cleanup” note in its description.</li>
</ul>
<hr style="margin: 55px 0; border: none; height: 1px;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">Bottom line</h2>
<p style="margin-bottom: 2em; line-height: 1.9;">Memory leaks in n8n’s queue workers are almost always caused by <strong>unreleased large objects</strong> (payloads, custom node state) or <strong>buffered network responses</strong>. By instrumenting workers with <code>--inspect</code> and <code>heapdump</code>, streaming heavy data, enforcing strict memory limits, and using health‑check‑driven restarts, you can <strong>detect, isolate, and permanently resolve</strong> the leak—keeping your queue healthy and your automations running 24/7.</p>
Step by Step Guide to solve n8n queue mode memory leak
Who this is for: Platform engineers and DevOps practitioners running n8n in production who need reliable, long‑running queue workers. We cover this in detail in the n8n Queue Mode Errors Guide.
Quick Diagnosis
Symptom: Queue workers crash after 30‑60 min with “JavaScript heap out of memory” or a SIGKILL.
Quick fix
Increase the V8 heap: NODE_OPTIONS="--max-old-space-size=4096" in the worker container.
Enable the inspector (--inspect) and capture a heap snapshot with heapdump.
Pinpoint the offending node (usually a custom node or a huge JSON payload) and free its memory (delete, clear, or stream the data).
Add a health‑check that restarts any worker that runs longer than 45 min.
// Example: Clean up after heavy computation
items = items.map(item => {
const result = heavyComputation(item.json);
delete item.json.largePayload; // free original data
return { json: result };
});
global.gc && global.gc(); // trigger GC if --expose-gc is set
return items;
Enable --expose-gc only in staging environments to avoid production overhead.
3.4 Isolate memory‑intensive jobs with a dedicated worker pool
Add a second pool that only runs “heavy” jobs:
# .env for heavy‑worker pool
QUEUE_MODE=memory
WORKER_CONCURRENCY=2
WORKER_LABELS=heavy
In the workflow, set Execute on Worker Label → heavy. This isolates leaks to a subset of pods that can be cycled more aggressively.
4️⃣ Validation & Ongoing Monitoring
Tool
Metric
Alert Threshold
Prometheus (node_memory_Active_bytes)
Worker RSS
> 3.5 GiB (when limit = 4 GiB)
Grafana (Heap Used)
process_resident_memory_bytes
80 % of max-old-space-size
Sentry (error fingerprint)
JavaScript heap out of memory
Immediate ticket
Datadog (container restart count)
container_restart_total
> 2 per hour
Create a dashboard that shows **memory trend per worker** and **queue backlog**. Correlate spikes with workflow IDs (available via process.env.N8N_WORKFLOW_ID in logs).
5️⃣ Preventive Best Practices (Checklist)
Never store full payloads in workflowData or global longer than the node execution.
Stream files > 10 MiB; use /tmp and delete after use.
Limit concurrency (WORKER_CONCURRENCY) for jobs that parse large data sets.
Pin Node.js to LTS (e.g., v20.x) to benefit from V8 memory‑leak fixes.
Run npm audit on custom node packages; upgrade any that depend on outdated request or xml2js.
Enable health‑check auto‑restart (see §3.1).
Document any custom node that allocates > 50 MiB and add a “cleanup” note in its description.
Bottom line
Memory leaks in n8n’s queue workers are almost always caused by unreleased large objects (payloads, custom node state) or buffered network responses. By instrumenting workers with --inspect and heapdump, streaming heavy data, enforcing strict memory limits, and using health‑check‑driven restarts, you can detect, isolate, and permanently resolve the leak—keeping your queue healthy and your automations running 24/7.