How to Fix "n8n Out of Memory" Error (JavaScript Heap Overflow)
⏱️ 17 min read · 📥 Free template + 3 explanatory diagrams
You're watching docker logs n8n and the same red line keeps appearing: FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory. The container restarts, works for an hour, crashes again. Your workflows look fine in the editor — they only die in production.
There are 4 different "out of memory" failures in n8n, and they need different fixes: (1) JavaScript heap overflow — raise the limit with NODE_OPTIONS=--max-old-space-size=4096; (2) container killed with exit code 137 — set Docker memory limits + a swap file; (3) Redis OOM in queue mode — set maxmemory with an LRU policy; (4) "payload too large" — raise N8N_PAYLOAD_SIZE_MAX or chunk the data.
The permanent fix for all four is the same: restructure the workflow so memory usage stops growing with data size — Split In Batches, sub-workflows, binary streaming. Section 12 gives you a free watchdog template that alerts you before the next crash, and section 10 covers three production switches no other guide mentions.
On this page
- How n8n Memory Works (3 Layers)
- Diagnose in 60 Seconds: 4 Signatures
- Prerequisites
- Fix 1: Raise the Heap (NODE_OPTIONS)
- Fix 2: Docker Limits & Swap (Exit 137)
- Fix 3: Fix the Workflow (Permanent)
- Fix 4: Queue Mode Isolation
- Fix 5: Redis OOM (Queue Mode)
- Find Which Node Eats Memory
- Production Tuning: Binary, Concurrency & Pruning
- 8 Common Errors + Fixes
- Free: Memory Watchdog Template
- Prevention Checklist
- FAQ
- Sources & References
1. How n8n Memory Works (3 Layers)
n8n is a Node.js application, and its memory has three layers. Understanding which layer failed tells you exactly which fix to apply:
| Layer | What it is | Controlled by | Fails with |
|---|---|---|---|
| 1. JavaScript heap | Memory for workflow data — items, arrays, JSON objects loaded by nodes | --max-old-space-size (via NODE_OPTIONS) |
FATAL ERROR: ... heap out of memory |
| 2. Container / process | Heap + buffers + binaries + the n8n process itself | Docker mem_limit / server physical RAM |
Exit code 137 (kernel OOM killer) |
| 3. Server-wide | Everything running on the VPS: n8n + Postgres + Redis + system | VPS RAM + swap file | Random container killed, dmesg shows Out of memory |
The critical relationship: your heap limit must stay comfortably below your container limit. Node needs memory outside the heap too — buffers, external libraries, the HTTP stack. Set the heap at 60–75% of the container's limit. If the heap limit exceeds the container limit, the kernel kills the whole process (error #2) before Node can even throw its own error (#1).
2. Diagnose in 60 Seconds: The 4 Signatures
Every "out of memory" report on n8n is one of these four. Find yours, then jump to its fix:
| # | Signature | Where you see it | Root cause | Fix |
|---|---|---|---|---|
| 1 | FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory |
docker logs n8n |
Heap layer full — workflow loaded too much data | Fix 1 then Fix 3 |
| 2 | Container restarts, exit code 137, no error in n8n logs |
docker ps -a + docker inspect |
Kernel OOM killer — container/server layer exhausted | Fix 2 |
| 3 | OOM command not allowed when used memory > 'maxmemory' |
Redis container logs | Redis hit its memory cap (queue mode only) | Fix 5 |
| 4 | Item size exceeds the maximum allowed size / webhook rejected |
n8n UI — execution error on a node | A single item exceeds N8N_PAYLOAD_SIZE_MAX (default 16 MB) |
Fix 3 (chunk) or raise it |
Run these four commands — they confirm which signature you have in under a minute:
# 1. Heap overflow? Look for the FATAL ERROR line
docker logs n8n --tail 200 | grep -i "heap out of memory"
# 2. Killed by the kernel? 137 = OOM killer
docker inspect --format='{{.State.ExitCode}} {{.State.Status}}' n8n
# 3. Kernel confirmed the kill? Check the system log
sudo dmesg | grep -i "out of memory" | tail -5
# 4. Live memory usage right now
docker stats --no-stream n8n
free -h
3. Prerequisites
- SSH access to the VPS running n8n (or your Docker host).
- Docker + Docker Compose — the snippets assume the standard stack from our n8n Self-Hosted with Docker installation guide.
- 15 minutes for the quick fix; 1–2 hours if you restructure heavy workflows.
4. Fix 1: Raise the Heap (Signature #1)
Node lets you raise the heap limit with --max-old-space-size (in megabytes). n8n accepts it through the standard NODE_OPTIONS environment variable.
Add NODE_OPTIONS to your n8n container
In docker-compose.yml, add the variable to the n8n service — and to n8n-worker if you run queue mode.
services:
n8n:
image: docker.n8n.io/n8nio/n8n
environment:
# 4096 MB = 4 GB heap — right for a 6–8 GB VPS
- NODE_OPTIONS=--max-old-space-size=4096
# Allow single items bigger than the 16 MB default (optional)
- N8N_PAYLOAD_SIZE_MAX=64
# ...rest of your config
Recreate the container
Run docker compose up -d — environment changes require recreating the container, not just restarting.
Verify the new limit
Confirm the flag is active inside the container:
docker exec n8n node -e "console.log(Math.round(require('v8').getHeapStatistics().heap_size_limit / 1024 / 1024) + ' MB')"
Heap sizing table (per VPS RAM)
| VPS RAM | Recommended heap | Container mem_limit | Swap file | Verdict |
|---|---|---|---|---|
| 1 GB | 512 | 700m | 1 GB | SQLite, low traffic only |
| 2 GB | 1024–1536 | 1.5g | 1 GB | Fine for light workflows |
| 4 GB | 2048–3072 | 3g | 2 GB | Sweet spot for most users |
| 8 GB | 4096–5120 | 6g | 2 GB | Queue mode capable |
NODE_OPTIONS — your only levers are the workflow-level fixes in section 6.5. Fix 2: Docker Limits & Swap (Signature #2 — Exit Code 137)
Exit code 137 means the kernel OOM killer terminated the container: the whole system ran out of memory, not just the heap. Raising NODE_OPTIONS won't help here — and can make it worse. Two fixes:
5.1 Explicit container limits
Limits stop one container from eating the whole server and make OOM behavior predictable:
services:
n8n:
image: docker.n8n.io/n8nio/n8n
# Hard cap: container is OOM-killed at 4 GB
mem_limit: 4g
# Soft hint: Docker tries to keep it around 1 GB
mem_reservation: 1g
# ...rest of your config
# Give the OTHER containers caps too — Postgres and Redis
# share the same physical RAM:
postgres:
image: postgres:16-alpine
mem_limit: 2g
mem_reservation: 512m
5.2 Add a 2 GB swap safety net
Swap doesn't fix the root cause, but it buys you time when memory spikes — the OS pages to disk instead of killing processes instantly:
# Create and enable a 2 GB swap file (Ubuntu/Debian)
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# Make it permanent
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
6. Fix 3: Fix the Workflow (The Permanent Solution)
Raising the heap postpones the crash. These five patterns stop the growth — a workflow that handles 50 items and 50,000 items with nearly the same memory footprint.
6.1 Use the built-in Split In Batches node
The n8n-native solution for large lists: the Split In Batches node processes items in batches of N within one execution, keeping only the current batch in flight. Drag it right after your trigger, set Batch Size to 100–500, and connect the loop output back to it:
- Batch Size 100 — safe default for heavy nodes (HTTP, AI, Sheets)
- Batch Size 500 — fine for lightweight transformations
- Combine with the Limit node upstream to cap total items per execution
6.2 Manual chunking in a Code node
When you need full control over the batching logic:
// Code node — process items in chunks of 100
const items = $input.all();
const CHUNK = 100;
const results = [];
for (let i = 0; i < items.length; i += CHUNK) {
const batch = items.slice(i, i + CHUNK);
// Process the batch — keep ONLY what the next node needs
results.push(...batch.map(item => ({ json: { id: item.json.id } })));
}
return results.map(r => ({ json: r.json }));
6.3 Split heavy logic into sub-workflows
Independent heavy logic (PDF processing, AI scoring, big API pulls) belongs in a separate workflow called via the Execute Workflow node. The sub-workflow gets its own execution, its own memory window, and — in queue mode — its own worker. If it crashes, the parent receives the error instead of dying with it.
6.4 Stream binaries instead of loading them into memory
Files are the #1 silent memory killer. A 200 MB CSV read as JSON text lands entirely in the heap; the same file streamed as binary barely touches it:
- In the HTTP Request node, set Response Format: File so downloads stream to binary storage.
- In Google Drive / S3 / FTP nodes, use their native file operations instead of converting to base64 text.
- Never
JSON.stringifya binary item inside a Code node — that copies the whole file into the heap.
6.5 Stop carrying data you don't need
- Use the Limit node to cap items per execution (e.g., 1,000).
- Use Remove Duplicates early — don't let 8,000 duplicates flow through 12 nodes.
- In Code nodes, return only the fields the next node needs (see the snippet above — we kept
id, dropped everything else). - Never accumulate arrays across loop iterations — write to the destination inside the loop instead.
- Clear large values after use:
delete item.json.hugeField.
7. Fix 4: Queue Mode — Isolate the Crash
When a memory-hungry execution is unavoidable (huge reports, long AI agent runs), the right architecture is queue mode: executions run on separate worker containers, each with its own heap and memory limit. A crash kills one worker — the UI, webhooks, and other workers keep running.
services:
n8n-worker:
image: docker.n8n.io/n8nio/n8n
command: worker
environment:
# Per-worker heap — size it like the main process
- NODE_OPTIONS=--max-old-space-size=4096
- EXECUTIONS_MODE=queue
# Parallel jobs per worker — lower this for memory-heavy workflows
- QUEUE_WORKER_CONCURRENCY=5
mem_limit: 5g
# ...Redis + Postgres config per our queue mode guide
QUEUE_WORKER_CONCURRENCY=10 runs 10 executions simultaneously — 10 memory footprints at once. For memory-heavy workflows, drop concurrency to 3–5 instead of raising the heap. The full architecture is in our Queue Mode with Redis & Workers guide, and when a worker does crash, the Error Handling guide makes it alert you instead of failing silently.8. Fix 5: Redis OOM (Signature #3 — Queue Mode Only)
If Redis hits its memory cap in queue mode, it rejects writes and jobs start failing or vanishing. Cap it cleanly with an LRU eviction policy instead of letting it crash:
services:
redis:
image: redis:7-alpine
command: ['redis-server', '--appendonly', 'yes', '--maxmemory', '2gb', '--maxmemory-policy', 'allkeys-lru']
volumes:
- redis_data:/data
Also limit how much history Redis keeps — completed-job metadata grows forever by default:
environment:
# Keep only the last 1000 completed jobs' metadata
- QUEUE_BULL_JOB_OPTIONS_REMOVE_ON_COMPLETE=1000
9. Find Which Node Eats Your Memory
Nobody tells you how to find the culprit node. Here's the 3-step method we use:
Read the execution view
Open the failed execution in n8n. The UI shows how many items and how much data each node passed. The node where item count or data size explodes is your culprit — usually an HTTP Request, Sheets read, or a Code node that accumulates arrays.
Probe with a temporary Code node
Drop this after a suspect node and re-run. It reports the live heap — no external tools needed:
// Temporary memory probe — place after a suspect node, then delete
const mem = process.memoryUsage();
return [{
json: {
heapUsedMB: Math.round(mem.heapUsed / 1024 / 1024),
rssMB: Math.round(mem.rss / 1024 / 1024),
items: $input.all().length
}
}];
Check binary data
If the suspect node passes files, confirm they're streaming as binary (section 6.4). A file converted to text or base64 in a Code node is the most common hidden culprit.
Relative memory profile of common nodes — use this to pick your first suspect before probing:
| Node type | Relative memory | Why |
|---|---|---|
| HTTP Request (large JSON/text response) | High | The whole response body lives in the heap |
| Google Sheets / database reads (big ranges) | Medium–High | The result set is materialized in memory |
| Code node accumulating arrays in a loop | High | The #1 self-inflicted leak — arrays grow across iterations |
| AI Agent / LangChain (long conversations) | Medium–High | Full conversation context per execution |
| IF / Switch / Set / Edit Fields | Negligible | Transformations pass data through |
| File nodes with binary streaming | Low | Streams to disk instead of the heap |
10. Production Tuning: Binary Mode, Concurrency & Pruning
Three switches that prevent memory problems before they start. None of the competitor articles on this error mention any of them:
10.1 Store binary data on disk, not in memory
By default, n8n keeps binary data (files, images, PDFs) in its database — which means every file your workflow touches gets loaded into the process. The filesystem mode writes files to disk instead:
environment:
# Files land on disk instead of the process memory/database
- N8N_DEFAULT_BINARY_DATA_MODE=filesystem
# Cap single items (default 16 MB) and webhook file uploads (default 200 MB)
- N8N_PAYLOAD_SIZE_MAX=64
- N8N_FORMDATA_FILE_SIZE_MAX=256
filesystem mode first.10.2 Cap concurrent executions
Memory = concurrency × footprint per execution. By default n8n runs unlimited production executions in parallel. Ten heavy executions at once on a 2 GB VPS is a guaranteed OOM — no heap setting saves you from arithmetic:
environment:
# Max simultaneous production executions (default: unlimited)
- N8N_CONCURRENCY_PRODUCTION_LIMIT=5
docker stats.10.3 Prune execution data before the database eats your RAM
n8n stores the full data of every execution forever unless you prune it. The executions table grows without bound: the UI slows down, history queries lag, and Postgres consumes more and more RAM — then users report "n8n is slow" without ever seeing a memory error:
environment:
# Auto-delete old execution data (default: off)
- EXECUTIONS_DATA_PRUNE=true
# Keep 7 days of history (default: 336 hours = 14 days)
- EXECUTIONS_DATA_MAX_AGE=168
# Hard cap on stored executions
- EXECUTIONS_DATA_PRUNE_MAX_COUNT=50000
# Don't store data for successful runs — failures only
- EXECUTIONS_DATA_SAVE_ON_SUCCESS=none
- EXECUTIONS_DATA_SAVE_ON_ERROR=all
EXECUTIONS_DATA_SAVE_ON_SUCCESS=none means no data view for successful runs. Fine once your workflows are stable — keep all while you're still debugging.10.4 Quick math: will this workflow fit in the heap?
Estimate before you run — one probe with real data tells you if chunking is needed:
// Estimation probe — run once with real input data, then delete
const items = $input.all();
const bytesPerItem = JSON.stringify(items[0]?.json || {}).length;
const estPayloadMB = Math.round((bytesPerItem * items.length) / 1024 / 1024);
return [{ json: { items: items.length, bytesPerItem, estPayloadMB } }];
Rule of thumb: if estPayloadMB is more than ~10% of your heap (e.g., 100 MB on a 1 GB heap), chunk it — every downstream node keeps its own working copy, so the real footprint is several times your estimate. If the payload is huge but each item is tiny, use Split In Batches; if each item is huge, that's a streaming problem (section 6.4).
11. The 8 Most Common Errors & Fixes
| # | Error / Symptom | Cause | Fix |
|---|---|---|---|
| 1 | FATAL ERROR: ... JavaScript heap out of memory |
Heap limit reached | Raise NODE_OPTIONS (section 4), then fix the workflow (section 6) |
| 2 | Exit code 137, no error in logs |
Kernel OOM killer — system memory exhausted | Container limits + swap + audit other containers (section 5) |
| 3 | Crash only when a specific workflow runs | That workflow's memory scales with its input size | Split In Batches / sub-workflow / streaming (section 6) |
| 4 | Item size exceeds the maximum allowed size |
One item is larger than N8N_PAYLOAD_SIZE_MAX (16 MB default) |
Raise the variable carefully, or chunk the payload (section 6.2) |
| 5 | Crash every night at the same time | A scheduled batch job (Sheets sync, report build) hits peak data | Chunk the batch + probe the node (sections 6 & 9) |
| 6 | Redis OOM command not allowed |
Redis maxmemory reached (queue mode) | maxmemory + allkeys-lru + remove-on-complete (section 8) |
| 7 | Execution killed by timeout, often misreported as "crash" | Not memory at all — the workflow exceeded its time budget | See our Check URL Node Timeout guide |
| 8 | n8n runs fine, but the UI gets slower every week; Postgres RAM climbs | Execution data bloat — every run's full data is stored forever | Prune it: EXECUTIONS_DATA_PRUNE=true + MAX_AGE + SAVE_ON_SUCCESS=none (section 10.3) |
12. Free Template: The Memory Watchdog
Every fix above is reactive — you find out after the crash. This watchdog is proactive: it probes n8n's real memory every 5 minutes and sends a Telegram alert before you hit the danger zone. Import the JSON below, set your chat ID, and adjust the threshold to ~60% of your container limit. Here is exactly what it does:
{
"name": "n8n Memory Watchdog",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 5
}
]
}
},
"id": "schedule",
"name": "Every 5 Minutes",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [250, 300]
},
{
"parameters": {
"jsCode": "const mem = process.memoryUsage();\nconst heapMB = Math.round(mem.heapUsed / 1024 / 1024);\nconst rssMB = Math.round(mem.rss / 1024 / 1024);\nconst heapLimitMB = Math.round(mem.heapTotal / 1024 / 1024);\nconst thresholdMB = 900;\nconst now = Date.now();\nconst lastAlert = $workflow.staticData.lastAlert || 0;\nconst alert = heapMB > thresholdMB;\nconst notify = alert && (now - lastAlert > 900000);\nif (notify) { $workflow.staticData.lastAlert = now; }\nreturn [{ json: { heapMB, rssMB, heapLimitMB, thresholdMB, alert, notify, timestamp: new Date().toISOString() } }];"
},
"id": "probe",
"name": "Probe Memory",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [450, 300]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.notify }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
"id": "if-alert",
"name": "Notify?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [650, 300]
},
{
"parameters": {
"chatId": "YOUR_TELEGRAM_CHAT_ID",
"text": "=🧠 n8n MEMORY ALERT\n\nHeap used: {{ $json.heapMB }} MB\nHeap limit: {{ $json.heapLimitMB }} MB\nRSS: {{ $json.rssMB }} MB\nThreshold: {{ $json.thresholdMB }} MB\nTime: {{ $json.timestamp }}"
},
"id": "telegram",
"name": "Telegram Alert",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [850, 200],
"credentials": {
"telegramApi": {
"id": "YOUR_TELEGRAM_CREDENTIAL_ID"
}
}
},
{
"parameters": {},
"id": "noop-ok",
"name": "Memory OK",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [850, 400]
}
],
"connections": {
"Every 5 Minutes": {
"main": [
[
{
"node": "Probe Memory",
"type": "main",
"index": 0
}
]
]
},
"Probe Memory": {
"main": [
[
{
"node": "Notify?",
"type": "main",
"index": 0
}
]
]
},
"Notify?": {
"main": [
[
{
"node": "Telegram Alert",
"type": "main",
"index": 0
}
],
[
{
"node": "Memory OK",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true
}
}
- Threshold:
thresholdMB = 900in the Probe node. Set it to ~60% of your container limit (e.g., 2400 for a 4 GB container). - Cooldown: alerts at most once per 15 minutes — no spam during a bad hour.
- Queue mode note: in queue mode this workflow runs on whatever worker picks it up, so it reports that worker's memory — run it on your busiest instance.
- Want more channels? Duplicate the Telegram node for Slack or email — the logic is channel-agnostic. More ready-to-import blueprints live in our n8n Workflow Templates Library.
13. Prevention Checklist
- ✅
NODE_OPTIONS=--max-old-space-sizesized to 60–75% of container memory - ✅ Explicit
mem_limiton n8n, Postgres, and Redis containers - ✅ A small (2 GB) swap file as a safety net
- ✅ Split In Batches on every workflow touching large lists
- ✅ Heavy logic isolated in sub-workflows (Execute Workflow node)
- ✅ Files streamed as binary, never stringified in Code nodes
- ✅ Limit nodes capping items per execution
- ✅ Queue mode workers for long-running executions, with sane concurrency
- ✅ The Memory Watchdog running and alerting before the danger zone
- ✅ Error Trigger workflow notifying on every crash (see the Error Handling guide)
- ✅
N8N_DEFAULT_BINARY_DATA_MODE=filesystemfor file-heavy workflows - ✅
N8N_CONCURRENCY_PRODUCTION_LIMITsized to your RAM - ✅ Execution data pruning enabled (
EXECUTIONS_DATA_PRUNE=true)
14. Frequently Asked Questions
It means the Node.js process running n8n exhausted the memory allocated to its JavaScript heap. The process aborts and Docker restarts it. It usually happens when a workflow loads too much data into memory at once — not because your server is broken.
Add NODE_OPTIONS=--max-old-space-size=4096 to the n8n container environment (4 GB for a 6–8 GB VPS) and recreate the container with docker compose up -d. Optionally set mem_limit on the container. See section 4 for the full sizing table.
The heap is the memory area Node.js uses for JavaScript objects, capped by --max-old-space-size. The container also uses memory outside the heap for buffers, libraries, and the process itself. Keep the heap at 60–75% of the container limit so the process never triggers the kernel OOM killer.
Because the crash is data-dependent. A workflow that processes 50 items runs fine; the same workflow with 5,000 items loads 100× more data into memory. The permanent fix is Split In Batches, sub-workflows, and binary streaming — see section 6.
It can. In queue mode, heavy executions run on separate worker containers with their own memory, so a crash kills one worker instead of your whole n8n. You can also scale workers horizontally. It requires PostgreSQL and Redis — see our queue mode guide.
It's the maximum size of a single data item n8n keeps in memory, in MB (default 16). Webhook payloads or HTTP responses larger than this cause errors. Increase it carefully — bigger limits mean more memory used.
Swap is a safety net, not a fix. It stops the OS from killing the process during memory spikes, but heavy swapping makes n8n extremely slow. Use a small swap file (2 GB) and fix the real cause: heap limit and workflow structure.
On n8n Cloud you can't change the heap limit, so use workflow-level fixes only: Split In Batches for large lists, Execute Workflow sub-workflows for heavy logic, binary streaming for files, and the Limit node to cap items per execution.
Yes — often more than raising the heap. By default n8n keeps binary data (files, images, PDFs) in its database, which loads them into the process. Setting N8N_DEFAULT_BINARY_DATA_MODE=filesystem writes files to disk instead, freeing memory in file-heavy workflows (section 10.1).
Check your executions table. n8n stores the full data of every execution forever unless pruned — the table grows, queries slow down, and Postgres consumes more RAM. Enable EXECUTIONS_DATA_PRUNE=true with a EXECUTIONS_DATA_MAX_AGE and EXECUTIONS_DATA_SAVE_ON_SUCCESS=none (section 10.3).
Sources & References
- Node.js Documentation — --max-old-space-size
- n8n Documentation — General environment variables
- n8n Documentation — Executions environment variables
- n8n Documentation — Queue mode
- Docker Documentation — Compose service configuration (mem_limit)
