Why n8n crashes on memory, and not for the reason you think
V8, the JavaScript engine bundled with Node.js, has a heap cap that is independent of physical RAM. By default it ranges from 512 MB to 1.5 GB depending on the Node version and platform — on a 4 GB or 8 GB VPS, the machine is not out of memory, but the V8 process is. Adding more RAM to the server changes nothing without NODE_OPTIONS=--max-old-space-size.
A second common cause: in main mode (the default), n8n executes workflows in the same process that serves webhooks and the API. A data-heavy workflow — CSV transformation, aggregation of thousands of rows, looping GPT calls — monopolises the heap during execution. If several pile up, the V8 cap is hit and the process is killed.
A third, subtler cause: jobs accumulate in memory when queue mode is enabled without dedicated workers. The queue (Redis or BullMQ) offloads executions from the main process, but if no worker actually consumes the jobs, they pile up, callbacks stay pending and the heap grows.
Signals that confirm an n8n OOM crash
- Exact exit message:
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryin container logs (docker logs n8n) - Silent crash under systemd: the service restarts automatically without leaving a trace if
Restart=alwaysis set — checkjournalctl -u n8n --since "1 hour ago" - Kernel OOM kill:
dmesg | grep -i oomshowsKilled process <pid> (node)before the restart, independently of Node - Correlation with a heavy workflow: the crash always occurs during executions of type "file processing" or "loop over thousands of items"
- Heap stable for hours then sudden spike: a periodically triggered workflow accumulates unreleased closures — the heap rises on each run and does not fully come back down
- Normal memory load in htop: system RAM is not saturated at the time of the crash, confirming the problem is V8, not the machine
Prerequisites before intervening
These steps assume n8n is already running on your VPS via Docker Compose. If that is not the case, the installer-n8n-vps article covers the complete deployment from scratch — come back here once the instance is up.
What you need to apply the fixes:
- Root SSH access to the VPS and an editable docker-compose.yml
- Available memory: a V8 cap of 4,096 MB (--max-old-space-size=4096) requires at least 6 GB of RAM on the VPS to leave headroom for the operating system, workers and Redis
- Redis already deployed if you switch to queue mode — redis:7-alpine is sufficient for single-VPS use
- n8n version 1.0 or higher: main/worker separation has been available since version 0.214 but is only stable in production from 1.0 onwards
- Database backup before any Compose modification — credentials and execution tables are not part of the Docker image
Fixing OOM: from diagnosis to stable configuration
Confirm the cause in logs
Read the last 200 lines of the container at the time of the crash:
docker logs n8n --tail 200 2>&1 | grep -E "FATAL|heap|OOM|Killed"If you see
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory, it is a V8 crash. If you seeKilledalone without a Node message, it is the kernel OOM killer — both can coexist on the same incident.Set the V8 cap via NODE_OPTIONS
In your
docker-compose.yml, add theNODE_OPTIONSenvironment variable to the n8n service. Recommended value based on VPS RAM:- VPS 4 GB:
--max-old-space-size=2048
- VPS 8 GB:--max-old-space-size=4096
- VPS 16 GB:--max-old-space-size=8192Rule: reserve roughly half the RAM available after the operating system and ancillary services (Redis, proxy). Do not exceed 70% of total RAM.
services: n8n: image: n8nio/n8n:latest environment: - NODE_OPTIONS=--max-old-space-size=4096 # ... other variablesVerify the value is actually read
After
docker compose up -d, verify Node reads the cap:docker exec n8n node -e "const v8=require('v8'); console.log(v8.getHeapStatistics().heap_size_limit / 1024 / 1024, 'MB')"The displayed value should be close to your
--max-old-space-size. If it still shows 512 or 1500, the environment variable is not being passed to the process — verify thatNODE_OPTIONSis in theenvironment:section of the service, not inenv_file:with a badly parsed value.Switch to queue mode with Redis
mainmode (default) runs everything in a single process. For instances handling more than 20 simultaneous workflows or large payloads, separate the roles:services: redis: image: redis:7-alpine restart: unless-stopped volumes: - redis_data:/data n8n: image: n8nio/n8n:latest environment: - NODE_OPTIONS=--max-old-space-size=2048 - EXECUTIONS_MODE=queue - QUEUE_BULL_REDIS_HOST=redis - QUEUE_BULL_REDIS_PORT=6379 depends_on: - redis ports: - "5678:5678" n8n-worker: image: n8nio/n8n:latest command: worker environment: - NODE_OPTIONS=--max-old-space-size=4096 - EXECUTIONS_MODE=queue - QUEUE_BULL_REDIS_HOST=redis - QUEUE_BULL_REDIS_PORT=6379 depends_on: - redis scale: 2 volumes: redis_data:The
n8nservice becomes the main process (API + UI + webhooks) with a moderate cap. Then8n-workerservice handles executions with a higher cap. Thescale: 2directive starts two workers — adjust to your needs.Separate the webhook process if traffic requires it
On instances receiving many parallel webhooks, the main process can be saturated even without executing workflows. n8n offers a dedicated webhook mode:
n8n-webhook: image: n8nio/n8n:latest command: webhook environment: - NODE_OPTIONS=--max-old-space-size=1024 - EXECUTIONS_MODE=queue - QUEUE_BULL_REDIS_HOST=redis - QUEUE_BULL_REDIS_PORT=6379 - N8N_DISABLE_UI=true depends_on: - redis ports: - "5679:5678"Then configure your reverse proxy to route
/webhook/to port 5679 and the rest to port 5678 of the main process. Webhook mode is available from n8n 1.0.Enable explicit garbage collection for heavy workflows
For workflows that process large files or long loops, you can help V8 release memory more aggressively:
NODE_OPTIONS="--max-old-space-size=4096 --expose-gc"This exposes
global.gc()— n8n can call it between workflow steps. Combine it withEXECUTIONS_DATA_SAVE_ON_SUCCESS=noneif you do not need the execution history: retained execution data often represents 30–50% of the heap.Monitor the heap after the fix
Enable n8n metrics to observe heap evolution without manual intervention:
N8N_METRICS=true N8N_METRICS_PREFIX=n8n_The
/metricsendpoint (port 5678) then exposesnodejs_heap_size_used_bytesandnodejs_heap_size_total_bytes, compatible with Prometheus. A basic Grafana dashboard on these two metrics will alert you well before the next crash.
Limit execution payload size
The EXECUTIONS_DATA_MAX_SIZE parameter (in bytes, default: no limit) cuts an execution before it can overflow the heap. Recommended value for general-purpose instances: 16777216 (16 MB). A workflow exceeding this threshold fails cleanly instead of killing the entire process. Combine it with EXECUTIONS_DATA_PRUNE=true and EXECUTIONS_DATA_MAX_AGE=168 (one week) to avoid accumulation of past execution data.
Post-fix configuration: useful environment variables
Once OOM is resolved, these variables consolidate instance stability:
- N8N_DEFAULT_BINARY_DATA_MODE=filesystem — stores binary files on disk rather than in memory; essential for workflows handling large CSV or PDF files
- OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true — manual executions (triggered from the editor) also go through workers, avoiding heap pressure on the main process during testing
- N8N_RUNNERS_ENABLED=true and N8N_RUNNERS_MAX_CONCURRENCY=5 — enables the experimental task runner (n8n 1.10+) which isolates each execution in a subprocess, preventing a single workflow from consuming all available heap
- DB_POSTGRESDB_* — migrating from SQLite to PostgreSQL on high-volume instances: SQLite serialises all reads/writes and can block workers, amplifying memory pressure
Troubleshooting — real errors and their causes
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
Direct cause: the V8 heap has reached its cap. Solution: add NODE_OPTIONS=--max-old-space-size=<n> in the container environment variables, with a value calibrated to available RAM (see step 2).
Killed in logs, without a Node message
Cause: the Linux kernel OOM killer terminated the process before V8 could emit its message. Occurs when physical memory is actually exhausted — different from a pure V8 crash. Check with dmesg | grep -i oom. Reduce --max-old-space-size or add RAM, and enable EXECUTIONS_DATA_SAVE_ON_SUCCESS=none to lighten the footprint.
Workers do not consume jobs despite EXECUTIONS_MODE=queue
Common cause: DB_TYPE and database variables are not passed to workers. Each Compose service must have its own connection variables — the worker does not inherit the main process configuration. Check with docker exec n8n-worker env | grep DB_.
Heap climbs after each run and does not come back down
Cause: a closure holds a reference to a large array between executions. Enable --expose-gc in NODE_OPTIONS and add EXECUTIONS_DATA_SAVE_ON_SUCCESS=none. If the behaviour persists, switch to task runner mode (N8N_RUNNERS_ENABLED=true), which isolates each workflow.
Error: Redis connection failed after switching to queue mode
Cause: QUEUE_BULL_REDIS_HOST points to localhost instead of the Docker service name. In a Compose network, the main process and workers reach Redis by its service name (redis in the example above), not 127.0.0.1.
Resources and next steps
The official n8n documentation on memory errors (docs.n8n.io/hosting/scaling/memory-errors) details recommended --max-old-space-size values based on available RAM and lists scaling parameters. The GitHub issue n8n-io/n8n#17461 (OOM in production, opened March 2026, 80+ comments) documents real-world cases — including the correlation between CSV processing workflows and heap crashes — and configurations that have stabilised instances similar to yours.
If you manage multiple n8n instances for different clients, the main/worker separation described here is also the foundation of a multi-tenant architecture: each client can have their own worker pool with an independent V8 cap, without a heavy workflow from one account impacting others.