n8n Workflow Force-Stopped After Hours? Fix Out-of-Memory Crashes with Batch Processing
Introduction
If your n8n workflow runs for three to six hours and then dies with no node turning red, you are almost certainly looking at an out-of-memory kill, not a logic bug. This is the most confusing failure mode in self-hosted n8n: the canvas shows the execution as stopped, the logs mention nothing about your workflow code, and re-running the workflow fails in the same place. This tutorial is for self-hosters running Docker or npm on a VPS who own long-running workflows, big API pulls, or file-heavy pipelines. You will learn to confirm the cause from the logs, apply the three fixes that matter, and rebuild the workflow so it no longer depends on holding everything in memory at once.
Quick answer
Disable execution-progress saving, process data in small batches through sub-workflows, and cap the Node.js heap with NODE_OPTIONS=–max-old-space-size. These three fixes resolve most long-running n8n workflow out-of-memory kills.
Evidence
Start with how much memory n8n actually needs, because the kill feels surprising on a box that seemed fine. n8n’s example sizing figures, which n8n labels for illustrative purposes and bases on n8n Cloud, span 320 MB to 2 GB of memory, with roughly 100 MB of memory for an idle instance. n8n’s Hetzner deployment guide goes further and says a 2 GB / 2 vCPU box (the CPX11 type) is enough for most usage levels. For raw throughput, n8n’s benchmarking documentation measured up to 220 workflow executions per second on a single instance, tested on a 4 GB AWS c5a.large with Postgres. The gap between “idles at 100 MB” and “dies after five hours” is your data, not the platform.
n8n’s official memory guide puts the root cause in one sentence: “n8n doesn’t restrict the amount of data each node can fetch and process. While this gives you freedom, it can lead to errors when workflow executions require more memory than available.” The guide lists the telltale error messages, including “Execution stopped at this node (n8n may have run out of memory while executing it)”, and adds that messages like “Problem running workflow”, “Connection Lost”, or “503 Service Temporarily Unavailable” suggest the whole instance became unavailable. In server logs you may also see the classic Node.js message “Allocation failed - JavaScript heap out of memory”.
The same official guide names the exact factors that inflate an execution’s memory: the amount of JSON data, the size of binary data, the number of nodes, memory-heavy Code nodes (which the docs call out explicitly, along with the older Function node), manual executions (n8n makes a copy of the data for the frontend), and additional workflows running at the same time. Its prescription for the memory-hungry case is direct: “Split the data processed into smaller chunks. For example, instead of fetching 10,000 rows with each execution, process 200 rows with each execution.” On sub-workflows it explains the mechanism plainly: “the sub-workflow only holds the data for the current batch in memory, after which the memory is free again.” Every fix below maps to one of these official recommendations, which are documented in n8n’s guide to fixing memory issues.
Prerequisites
- A self-hosted n8n instance (Docker or npm) on a VPS or home server with SSH access.
- A workflow that runs for hours and dies before finishing. If you only suspect memory, the diagnosis step below confirms it in minutes.
- Basic comfort editing a docker compose file or systemd service, and reading container logs.
Step 1: Confirm the kill is out-of-memory
Do not restructure anything until the logs agree. Check these three places in order.
First, open the failed execution in the n8n editor. If a node shows “Execution stopped at this node (n8n may have run out of memory while executing it)”, you already have the answer from n8n itself.
Second, check the container or server logs around the time of death:
# Docker: look for the Node.js heap message
docker logs n8n 2>&1 | grep -iE "heap out of memory|killed|allocation failed" | tail -20
# npm / systemd: same search in the service journal
journalctl -u n8n --since "6 hours ago" | grep -iE "heap out of memory|killed" | tail -20
The two smoking guns are Allocation failed - JavaScript heap out of memory (the Node.js process hit its heap ceiling) and the kernel’s Killed process / exit code 137 line (the container or OS reclaimed the whole process). A process that simply vanishes with code 137 and no Node.js message means the container hit its memory limit before the heap did.
Third, watch memory during a run:
docker stats --no-stream n8n
If the MEM % column climbs steadily toward the limit and the container restarts near 100%, the diagnosis is confirmed. A flat line that suddenly drops points elsewhere, usually an external API or database dropping the connection. The official memory guide also lists “Connection Lost” and “503 Service Temporarily Unavailable” as signs the instance became unavailable, which matches the container-restart pattern.
Step 2: Stop saving execution progress (the quick win)
n8n saves node-level progress data by default, and for a six-hour execution that journal is enormous. This is the cheapest fix because it changes no workflow logic. n8n’s execution data management documentation recommends not saving unnecessary data, and provides the exact environment variables:
# docker-compose.yml, n8n service environment
environment:
# Do not save successful executions at all
- EXECUTIONS_DATA_SAVE_ON_SUCCESS=none
# Save executions that end in errors (keep this for debugging)
- EXECUTIONS_DATA_SAVE_ON_ERROR=all
# Do not save per-node progress for each execution (the big one)
- EXECUTIONS_DATA_SAVE_ON_PROGRESS=false
# Do not save manually launched executions
- EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS=false
Apply the same variables in a systemd unit with Environment= lines. Then restart:
docker compose up -d # Docker
sudo systemctl restart n8n # npm / systemd
Two details from the official documentation are worth knowing. Pruning is enabled by default: n8n deletes finished executions older than EXECUTIONS_DATA_MAX_AGE hours (default 336, that is 14 days) or when the total passes EXECUTIONS_DATA_PRUNE_MAX_COUNT (default 10,000), oldest first. And if you run the default SQLite database, pruning marks rows for deletion but the disk space is only reused, not freed, unless you configure the SQLite vacuum options.
Expect this fix to cut the memory of a progress-heavy long run noticeably, and expect it to make post-mortem debugging harder for successful runs, which is the intended trade. Keep EXECUTIONS_DATA_SAVE_ON_ERROR=all so failures still leave a trail.
Step 3: Batch the work through sub-workflows (the durable fix)
Reducing per-execution memory is the fix that scales, and the official guidance is to split the workflow into sub-workflows that each return a small result set. The pattern needs two extra nodes on the parent: a Loop Over Items node that splits items into batches, and an Execute Workflow node that calls the sub-workflow once per batch.
Rebuild the heavy part of your workflow like this:
- In the parent workflow, replace the node that fetches the giant dataset with a paginated fetch that returns only item references or page tokens (IDs, URLs, cursors), not full payloads.
- Add a Loop Over Items node set to a batch size of 100 to 200 items. The official docs use 200 rows per execution as their example chunk, replacing a single 10,000-row execution.
- Add an Execute Workflow node inside the loop that passes one batch to the sub-workflow.
- In the sub-workflow, fetch the full data for that batch, do the heavy processing, write the result to its final destination (database, file, API), and return only a tiny summary: batch id, rows processed, errors count.
The memory math is why this works. As the docs explain, the sub-workflow only holds the data for the current batch in memory, and once the batch is done that memory is freed. The parent never accumulates more than one batch’s worth of results at a time, so a job that processes a million rows uses roughly the same memory as one that processes two hundred. When the write happens inside the sub-workflow and the returned data is small, the parent’s memory stays flat for the entire run.
If you use Code nodes for the heavy lifting, audit them first: n8n’s memory guide explicitly names the Code node as memory-heavy. A Code node that maps over a 50,000-item array holds the whole array plus the result in memory at once. Moving that logic into a batched sub-workflow, or rewriting it to process one batch per call, usually fixes Code-node OOMs without any server changes. Our fix for the n8n Code node timeout after the 3.0 upgrade covers the other common Code node failure mode.
Step 4: Cap the V8 heap so the process degrades instead of dying
Even a batched workflow can spike, and an uncapped Node.js process keeps growing until the kernel kills it with no graceful shutdown. Setting the old-space heap limit makes V8 garbage-collect earlier and fail loudly instead of being silently reaped. n8n’s documentation recommends setting the V8 option --max-old-space-size=SIZE either through the CLI or through the NODE_OPTIONS environment variable, as described in the environment variables reference.
# docker-compose.yml, 2 GB VPS example: cap the heap at ~1.5 GB
environment:
- NODE_OPTIONS=--max-old-space-size=1536
# systemd unit, same setting for npm installs
Environment=NODE_OPTIONS=--max-old-space-size=1536
Size the number to about 65 to 75 percent of the memory available to the n8n process. On a 2 GB VPS with Postgres on the same box, 1024 to 1536 MB is the sane range; on a 4 GB box, 2048 to 3072 MB. One behavior difference matters here: when n8n hits the wall, n8n’s Docker image and n8n Cloud restart automatically, while an npm install usually needs a manual restart, so pair this fix with a systemd Restart=always or a Docker restart: unless-stopped policy.
Step 5: Move binary data off the heap (if files are involved)
If your workflow moves files, images, or PDFs, there is a fourth lever. n8n keeps binary data in memory by default, and its documentation warns this can cause crashes when working with large files. Flip the mode to filesystem:
environment:
- N8N_DEFAULT_BINARY_DATA_MODE=filesystem
This writes binary payloads to disk instead of holding them in the Node.js heap, at the cost of disk I/O. It is the single most effective change for workflows that download and transform media or documents in a loop.
Troubleshooting
The container restarts with exit code 137 and no Node.js heap message. The container hit its memory limit before V8 hit its heap cap, which means the heap cap is set higher than the container limit or is unset. In Docker, V8 sizes its heap from the host’s RAM, not from the container’s cgroup limit, so an uncapped process inside a 2 GB container happily grows past 2 GB and gets reaped by the kernel. Fix: set NODE_OPTIONS=--max-old-space-size to roughly two-thirds of the container’s memory limit, not of the host’s.
Memory still grows after batching with sub-workflows. Check what the sub-workflow returns. If it returns the full processed dataset instead of a small summary, the parent accumulates every batch’s output and you have rebuilt the original problem with extra nodes. The sub-workflow should write results to their final destination and return only counts or IDs. Also confirm the Loop Over Items batch size is actually small; a “batch” of 5,000 items is not batching.
Manual test runs die but scheduled runs of the same workflow survive. Manual executions cost more memory because n8n keeps a copy of the data for the frontend. The official memory guide lists manual executions as a memory factor for this exact reason. For long jobs, stop testing the full dataset by clicking Execute; test on a 50-item sample manually, then let the scheduled or webhook trigger run the full volume.
A Code node OOMs even on modest data. Code nodes copy data before and after processing, so the peak is higher than the input size suggests. Avoid building giant arrays in one script: process in chunks inside the code, use the built-in nodes where they exist, or move the logic into the batched sub-workflow pattern from Step 3. If the node also runs long, check whether the 3.0 timeout change is involved.
Execution history eats the disk even after pruning. This is the SQLite behavior documented in the execution data guide: pruning deletes rows, but SQLite reuses the freed space instead of returning it to the OS. For a fresh database, migrate to Postgres, which is the documented production recommendation anyway; for an existing SQLite database, look into the DB_SQLITE_VACUUM_ON_STARTUP options.
FAQ
See the FAQ section in the front matter above for the six most-asked questions, covering RAM sizing, n8n Cloud behavior, heap-cap performance impact, the “dies after the same hours” pattern, queue mode versus batching, and whether more RAM alone is enough.
Conclusion
Long-running n8n workflows die after hours because every node result accumulates in memory until the process is killed, and the fix is to stop accumulating. Turn off execution-progress saving, batch the work through sub-workflows that return small results, cap the V8 heap, and move binary data to disk. If many workflows collide on one box rather than one workflow growing too large, the next step is our guide to scaling n8n with queue mode. For the full self-hosting picture, from installation to security, start with the complete n8n guide.
Frequently asked questions
How much RAM does a self-hosted n8n instance actually need?
A 2 GB VPS is the sensible floor for production use. n8n's own example sizing figures range from 320 MB to 2 GB of memory with roughly 100 MB for an idle instance, and n8n's Hetzner deployment guide says a 2 GB / 2 vCPU box (CPX11) is enough for most usage levels. Your workflows decide the ceiling: data volume, Code nodes, and large binary files push consumption up, not the n8n software itself.
Do n8n Cloud users hit these memory problems too?
They can, but the experience differs. On n8n Cloud and in n8n's Docker image, n8n restarts automatically when it hits a memory wall, so you see a restarted instance instead of a dead server; with an npm install you usually have to restart it manually. The official memory guide points Cloud users to the Cloud data management documentation for the memory limits of their plan, which you cannot change the way you can on a self-hosted box.
Will capping the V8 heap with --max-old-space-size slow n8n down?
It trades earlier garbage collection for stability. A heap cap forces V8 to clean up more often, which adds small CPU overhead, but that is far cheaper than a full process kill that throws away hours of progress. Set the cap to about 65 to 75 percent of your container memory limit so the operating system and n8n's non-heap memory have room, and monitor before raising it.
My workflow dies after the same number of hours every time. Is that a leak?
Usually it is accumulation, not a leak in n8n itself. Every node result stays in memory until the execution finishes, so a workflow that appends data for six hours simply grows until the box gives up. This is exactly the signature that batch processing fixes: process 200 items, write the result out, and let the batch memory be freed before the next batch starts.
Should I use queue mode instead of batching for long workflows?
They solve different problems. Queue mode spreads concurrent executions across worker processes and helps when many workflows run at once; batching reduces the peak memory of a single long execution. For one giant workflow that dies after hours, batch first. For many workflows colliding on a small box, read our guide to scaling n8n with queue mode for the full worker setup.
Can I just add more RAM instead of restructuring the workflow?
You can, and it is the fastest fix when the budget allows it. n8n's documentation lists provisioning more memory as the first of its two options. But RAM is a recurring cost and the fix does not scale: if your data grows 10x, so does the bill. Batching keeps per-execution memory roughly flat no matter how the data grows, which is why it is the durable answer for long-running jobs.