n8n execution history growing out of control: automatic pruning and getting your database size back
Published 3 August 2026 · 9 min read
The scenario is almost always the same: a self-hosted n8n instance that ran fine for months starts slowing down, the UI takes several seconds to display the executions list, and a df -h on the server reveals a Docker volume or a database weighing several gigabytes. The cause is structural, not accidental: by default, every workflow execution stores in the database the data of every node it went through — the full payloads, input and output alike. A ten-node workflow shuffling API responses of a few hundred kilobytes writes several megabytes per execution; an AI or RAG workflow handling entire documents, embeddings and LLM responses writes far more. Multiply by hundreds of executions per day, and the database balloons mechanically.
n8n does ship with automatic pruning, enabled by default. This guide explains how it actually works, how to tune it, why deleting executions doesn't always give the disk space back (SQLite is the trickiest case), and above all how to reduce what enters the database at the source — the only strategy that holds over time.
What n8n stores on every execution — and why it weighs so much
When a workflow runs, n8n records the execution itself (status, timestamp, which workflow) and, separately, the data produced by each node: the full JSON items exactly as you see them in the UI when you click a node of a past execution. That second part is what weighs. Being able to reopen a three-day-old execution and inspect the exact output of every node is a precious debugging comfort — but it's a comfort billed in gigabytes.
Three workflow profiles grow the database faster than the rest:
- AI and RAG workflows: source documents, chunks, embeddings, complete model responses — each execution can carry megabytes of text from one node to the next, and all of it gets kept.
- High-frequency workflows: a poll every minute produces 1,440 executions per day, even when there's nothing to process.
- File-handling workflows: if binary data stays in the database (the default mode), every PDF or image passing through the workflow is stored with the execution — our guide on handling large files and binary data covers that mechanism in detail.
None of this is specific to n8n. A reference study by Oliner, Ganapathi and Xu published in 2012 in Communications of the ACM ("Advances and challenges in log analysis" — see on Google Scholar) already made this observation about system logs in general: logs grow faster than the ability to make use of them, and the real question is never keeping everything but deciding what to retain and why. That's exactly the right frame for thinking about n8n's execution history: a diagnostic tool, not an archive.
Automatic pruning: EXECUTIONS_DATA_PRUNE and its two caps
n8n prunes old executions automatically, with no configuration. Three environment variables drive the mechanism:
# Automatic pruning (enabled by default)
EXECUTIONS_DATA_PRUNE=true
# Maximum age of a retained execution, in hours
# 336 by default = 14 days
EXECUTIONS_DATA_MAX_AGE=336
# Maximum number of retained executions
# 10000 by default
EXECUTIONS_DATA_PRUNE_MAX_COUNT=10000
Both caps apply jointly: an execution is pruned as soon as it exceeds the maximum age or the retained total exceeds the count cap. On a busy instance, EXECUTIONS_DATA_PRUNE_MAX_COUNT usually bites first; on a quiet one, the age does. For a typical small-business production, tightening both values is the first reflex:
# docker-compose.yml excerpt
services:
n8n:
image: docker.n8n.io/n8nio/n8n
environment:
- EXECUTIONS_DATA_PRUNE=true
- EXECUTIONS_DATA_MAX_AGE=168 # 7 days
- EXECUTIONS_DATA_PRUNE_MAX_COUNT=5000
A useful detail for interpreting what you observe: pruning works in two steps. The affected executions are first marked as deleted (a "soft delete" — they disappear from the UI), then actually erased from the database in a later pass. So it's normal not to see the database react the second a threshold is crossed. And as we'll see, even the actual deletion doesn't necessarily give the disk space back to the system.
These variables are set like any other — our n8n environment variables guide covers the different ways to inject them depending on your setup.
Reduce at the source: store only what's useful
Pruning caps the stock; the save variables cap the flow. It's the most powerful lever, because it acts before the data ever enters the database:
# Don't keep successful executions
EXECUTIONS_DATA_SAVE_ON_SUCCESS=none
# Keep all failed executions
EXECUTIONS_DATA_SAVE_ON_ERROR=all
# Don't save intermediate state during execution
# (false by default — leave it that way: every node would write to the database)
EXECUTIONS_DATA_SAVE_ON_PROGRESS=false
# Whether to keep executions launched manually from the editor
EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS=true
The SAVE_ON_SUCCESS=none + SAVE_ON_ERROR=all combination is the most common production setting: executions that go well leave no detailed trace, those that fail remain fully available for diagnosis. The trade-off is clear — you can no longer reopen a successful execution to inspect its data — and it's compensated by wiring up an error workflow that alerts you the moment a failure occurs, with the necessary context.
EXECUTIONS_DATA_SAVE_ON_PROGRESS deserves a word: set to true, n8n writes the execution's state to the database node by node, which lets you see where a long execution stands but multiplies writes. It's false by default; only enable it if you have a specific need.
These settings also exist per workflow, in the workflow settings (⋯ menu → Settings): Save successful production executions, Save failed production executions, Save manual executions, Save execution progress. That's the useful granularity when a single workflow — typically the RAG pipeline ingesting documents — accounts for most of the volume: set it to Do not save on success, and keep the global setting more permissive for the lightweight workflows.
The SQLite case: deleting doesn't give the space back
This is the pitfall that generates the most confusion. On a default installation, n8n uses SQLite, and the database.sqlite file never shrinks on its own: when rows are deleted (by pruning or by hand), SQLite marks the pages as reusable but keeps the file size. You can prune 90% of your executions and find, puzzled, that the Docker volume still weighs just as much.
The solution n8n provides is a VACUUM at startup:
DB_SQLITE_VACUUM_ON_STARTUP=true
On the next restart, SQLite rebuilds the file, compacting the freed space. Two warnings: the instance is unavailable for the whole duration of the VACUUM, and on a multi-gigabyte database the operation can take a long time. Do it during a quiet window, after backing up the file, and remember to set the variable back to false afterwards if you don't want to pay that cost on every restart.
Let's be direct about the underlying issue: if your database.sqlite reaches several gigabytes, the VACUUM treats the symptom, not the cause. SQLite is perfect for discovering n8n and for small loads, but past a certain execution volume, migrating to PostgreSQL is the real solution — better behavior under concurrency, mature measurement and maintenance tooling, clean backups. Our guide on backing up and restoring a self-hosted n8n on PostgreSQL covers the database side of the setup.
The PostgreSQL case: measure, then understand autovacuum
On PostgreSQL, the weight concentrates in two tables: execution_entity (the executions themselves) and above all execution_data (the node data). To measure precisely:
SELECT relname AS table,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 10;
If execution_data dominates this ranking by far — it almost always does — you know the subject really is execution history, not something else.
On reclaiming space, PostgreSQL behaves differently from SQLite, but with a comparable subtlety: autovacuum spots deleted rows and makes their space reusable for new writes, so a regularly pruned database stabilizes instead of growing indefinitely. However, autovacuum does not shrink the files on disk: the space stays allocated to the table. To actually return space to the system, you need a VACUUM FULL, which rewrites the table — but takes an exclusive lock on it for the whole operation: n8n can't write executions in the meantime. Reserve it for a planned maintenance window, instance stopped or traffic cut off, and only if you genuinely need the disk back (after a one-off massive purge, for instance). In steady state, well-tuned pruning plus autovacuum are enough: the database reaches its cruising size and stays there.
Binary data: get the files out of the database
If your workflows handle files (PDFs, images, exports), check the binary storage mode. By default, n8n keeps binary data with the execution data — so in the database. A single variable changes that behavior:
N8N_DEFAULT_BINARY_DATA_MODE=filesystem
Files are then written to disk (inside the n8n volume) rather than into the database, and pruning executions cleans up the associated files. On an instance processing documents, this is often the variable that changes the database size trajectory the most. The full mechanism, including the implications for backups and multi-instance setups, is covered in our binary data guide.
The strategy that holds in production
Putting the pieces together, the cruising configuration of a production instance looks like this:
services:
n8n:
image: docker.n8n.io/n8nio/n8n
environment:
# Flow: store only what's useful
- EXECUTIONS_DATA_SAVE_ON_SUCCESS=none
- EXECUTIONS_DATA_SAVE_ON_ERROR=all
- EXECUTIONS_DATA_SAVE_ON_PROGRESS=false
# Stock: short history
- EXECUTIONS_DATA_PRUNE=true
- EXECUTIONS_DATA_MAX_AGE=168
- EXECUTIONS_DATA_PRUNE_MAX_COUNT=5000
# Files out of the database
- N8N_DEFAULT_BINARY_DATA_MODE=filesystem
This setup rests on one principle: execution history is for recent diagnosis, not long-term traceability. Failures are retained and an error workflow flags them in real time; successes leave only their statistical trace. If a business need requires knowing durably what was processed (which customers, which documents, which amounts), write those few fields to a dedicated table from the workflow itself — a few bytes per execution instead of megabytes, and data that's actually usable.
What remains is checking the trajectory stays right: database size belongs among the metrics to track over time, alongside memory and queues — our guide on monitoring an n8n instance shows how to fold it into simple monitoring. And if the instance slows down despite a healthy database, the problem lies elsewhere: see our guide on optimizing workflow performance.
Common pitfalls
- Believing pruning gives the disk space back: it deletes rows, but
database.sqlitekeeps its size withoutDB_SQLITE_VACUUM_ON_STARTUP=true, and PostgreSQL reuses the space without returning it to the system short of aVACUUM FULL. - Running a VACUUM (SQLite) or a VACUUM FULL (PostgreSQL) in the middle of the day: one freezes the instance at startup, the other locks the table — both belong in a quiet window, backup done.
- Enabling
EXECUTIONS_DATA_SAVE_ON_PROGRESS=true"just in case": every node then writes to the database during execution, multiplying writes for a rarely needed benefit. - Tuning the global pruning but forgetting the workflow that weighs: a single RAG pipeline can account for most of the volume; its workflow settings (Save successful production executions → Do not save) fix the problem at the source.
- Leaving binary data in the database on an instance that processes files:
N8N_DEFAULT_BINARY_DATA_MODE=filesystemchanges the trajectory more than any pruning setting. - Staying on SQLite beyond what's reasonable: compacting a multi-gigabyte database again and again isn't a strategy; migrating to PostgreSQL is.
- Cutting all history with no safety net:
SAVE_ON_SUCCESS=nonewithout an error workflow or a business log means flying blind the day something goes wrong.
In summary
An n8n database that balloons isn't an anomaly, it's the default behavior: everything is stored, for every node, of every execution. The answer comes in three layers — reduce the flow (EXECUTIONS_DATA_SAVE_ON_SUCCESS=none, SAVE_ON_ERROR=all, binaries on the filesystem), cap the stock (EXECUTIONS_DATA_MAX_AGE and EXECUTIONS_DATA_PRUNE_MAX_COUNT tightened), and reclaim space at the right moment (SQLite VACUUM at startup, PostgreSQL autovacuum in steady state). As the study by Oliner and his co-authors already pointed out, the question is never keeping everything but knowing what to retain and why. That's precisely the logic of a proper audit trail: keeping a usable record of what was processed — who, what, when, with what outcome — without storing gigabytes of intermediate payloads. The Compliance & Audit Pack (€149) provides exactly that: ready-to-use n8n workflows to build this dedicated, lightweight and durable audit trail, while your execution history stays what it should be — a short-lived diagnostic tool.
FAQ
Frequently asked questions
Why does my n8n database keep growing even though automatic pruning is enabled?
Two explanations dominate. First: pruning deletes rows, but the database file doesn't shrink as a result — SQLite keeps the space inside database.sqlite until a VACUUM compacts the file, and PostgreSQL reuses freed space for new writes without returning it to the system. Second: pruning caps the age and count of retained executions, but if each execution stores huge payloads (AI workflows, RAG, binary files kept in the database), 10,000 recent executions are enough to weigh a lot. You then need to reduce what gets stored at the source with EXECUTIONS_DATA_SAVE_ON_SUCCESS=none and N8N_DEFAULT_BINARY_DATA_MODE=filesystem.
What's the difference between EXECUTIONS_DATA_MAX_AGE and EXECUTIONS_DATA_PRUNE_MAX_COUNT?
Both caps apply together. EXECUTIONS_DATA_MAX_AGE sets the maximum age of a retained execution, in hours (336 by default, i.e. 14 days): beyond that, it gets pruned regardless of how many there are. EXECUTIONS_DATA_PRUNE_MAX_COUNT sets the maximum number of retained executions (10,000 by default): beyond that, the oldest ones get pruned even if they haven't reached the age limit. On a busy instance, the count cap usually triggers first; on a quiet one, the age does.
Is it risky to set EXECUTIONS_DATA_SAVE_ON_SUCCESS=none in production?
It's a deliberate trade-off: you lose the ability to replay or inspect a successful execution after the fact, but you divide what enters the database by your workflows' success rate — usually the bulk of the volume. The common practice is to keep EXECUTIONS_DATA_SAVE_ON_ERROR=all so every failure is retained, wire up an error workflow to be alerted immediately, and record yourself the few business facts that must survive (processed IDs, timestamp, outcome) in a dedicated table or log rather than relying on the full execution history.
Bundle FlowKit Complet
€269