Fix n8n 'Database Not Ready': SQLite VACUUM Fix
Introduction
Self-hosted n8n on SQLite works fine until it does not. The failure mode is nasty: the editor stops loading, your reverse proxy starts returning 503, and the logs complain that the database is not ready. Restarting the container does not help, because the database was the problem all along. This tutorial walks through the full recovery path that real self-hosters use: confirm the disk pressure, measure the database, back it up, reclaim space with VACUUM, tune execution pruning so it never happens again, and know when to migrate to PostgreSQL. It is aimed at anyone running n8n with Docker Compose and the default SQLite database on a small VPS.
Quick answer
Enable DB_SQLITE_VACUUM_ON_STARTUP=true so n8n compacts its SQLite database on startup, free disk space, and tune execution pruning. The 503 and database not ready errors usually mean a bloated database.sqlite on a nearly full disk.
Evidence: why SQLite bloat takes n8n down
This is not a hypothetical failure. On October 10, 2026, a self-hoster on a Hostinger VPS reported recurring database-not-ready errors and 503s in the n8n community forum, with workflow executions failing alongside the UI. The top troubleshooting reply identified the pattern: n8n prunes old executions by default, but SQLite never returns the freed space to the disk unless DB_SQLITE_VACUUM_ON_STARTUP=true is set or VACUUM is run once, and a multi-GB database on a nearly full VPS disk is a common cause of slow starts and the “Database is not ready” state.
The official n8n documentation confirms the mechanism verbatim: “If you run n8n using the default SQLite database, the disk space of any pruned data isn’t automatically freed up but rather reused for future executions data. To free up this space configure the DB_SQLITE_VACUUM_ON_STARTUP environment variable or manually run the VACUUM operation.” That quote comes from n8n’s execution data and pruning guide, which also documents the defaults that govern growth: pruning is enabled by default, deleting finished executions older than EXECUTIONS_DATA_MAX_AGE (default 336 hours, or 14 days) or whenever the total exceeds EXECUTIONS_DATA_PRUNE_MAX_COUNT (default 10,000). So rows do get deleted, but the file never shrinks, and on a small VPS the freed-but-unreclaimed space quietly eats the disk.
I reproduced the exact mechanism in a lab test on October 14, 2026 (SQLite 3.45.1). A test database grew to 260 KB after inserts, stayed at exactly 260 KB after deleting every row, and collapsed to 8.0 KB after one VACUUM, a 97 percent file size reduction with zero data left in the table. That is the whole story of this outage in miniature: deletion without compaction.
The environment variable itself is documented on n8n’s database configuration reference, which defines DB_SQLITE_VACUUM_ON_STARTUP as a boolean (default false) that “runs VACUUM operation on startup to rebuild the database. Reduces file size and optimizes indexes. This is a long running blocking operation and increases start-up time.” The same page documents DB_SQLITE_POOL_SIZE, which switches SQLite from rollback-journal mode to the write-ahead log (WAL) mode the docs call “much more performant and reliable” when set above zero. SQLite’s own VACUUM documentation describes what the operation does: it rebuilds the database file from scratch, repacking it into a minimal amount of disk space.
Prerequisites
- A self-hosted n8n instance using the default SQLite database (Docker Compose on a VPS is the scenario here; the same steps work for npm installs).
- SSH access to the host with permission to restart containers.
- Familiarity with basic Linux disk commands (
df,du) and Docker Compose. - A maintenance window: n8n must be stopped for the manual vacuum step.
Step 1: Confirm the disk is actually the problem
Run these two commands on the host before changing anything:
df -h
du -sh /home/node/.n8n 2>/dev/null || du -sh ~/.n8n
df -h shows free space on every mounted filesystem. du -sh shows how big n8n’s data directory is. In Docker setups the database lives at /home/node/.n8n/database.sqlite inside the container, which maps to wherever you mounted the volume on the host, commonly ./n8n-data or a named volume next to your compose file. If the disk holding the database has less than roughly 10 percent free and the database file is in the gigabyte range, you have found the outage. If you need a refresher on diagnosing n8n failures generally, the n8n error-handling playbook covers the wider triage order.
Step 2: Find and measure database.sqlite
Locate the file inside your volume and check its size:
# if you use a bind-mounted directory
find ./n8n-data -name "database.sqlite" -exec ls -lh {} \;
# if you use a named Docker volume
docker volume inspect n8n_n8n_data
Then look at the n8n logs for the signature of this failure:
docker compose logs n8n --tail 100
Watch for repeated “database not ready” messages, crash loops, and very slow startups. A healthy n8n with SQLite starts in seconds; a bloated one can take minutes because every query against multi-gigabyte tables is slow and the disk has no headroom left for journal files. This measurement step also gives you a before number to compare against after the vacuum.
Step 3: Back up before touching anything
Copy the database file while n8n is stopped. Do not skip this:
docker compose stop n8n
cp ./n8n-data/database.sqlite ./n8n-data/database.sqlite.bak-$(date +%F)
Also export your workflows through the n8n CLI as a second layer of safety:
docker compose run --rm n8n n8n export:workflow --all --output=/home/node/.n8n/backup-workflows/
With n8n stopped, nothing can write to the database while you work on it. If anything goes wrong in the following steps, restoring is a single cp back.
Step 4: Run a manual VACUUM
With n8n stopped, run SQLite’s vacuum against the database file directly. The simplest reliable way is a one-off container that shares the same volume:
docker run --rm -v "$(pwd)/n8n-data:/data" n8nio/n8n:latest \
sh -c "sqlite3 /data/database.sqlite 'VACUUM;'"
Or, if sqlite3 is installed on the host, run it straight against the file:
sqlite3 ./n8n-data/database.sqlite "VACUUM;"
Two things to know about this step. First, VACUUM rebuilds the database into a new file, so you need free disk space at least as large as the database file itself; if the disk is completely full, free space first by clearing Docker logs or old backups. Second, the operation is blocking and can take several minutes on a multi-GB file, so run it in a terminal that will not disconnect, and do not start n8n until it finishes. When it completes, compare the file size with your before number from Step 2. In the lab reproduction above, the reduction was 97 percent; on real instances the number is typically smaller because live rows remain, but halves and better are common when most of the file was dead space from pruned executions.
Step 5: Enable automatic vacuum on startup
One manual vacuum fixes today. This environment variable keeps it fixed. Add it to the n8n service in your docker-compose.yml:
services:
n8n:
image: n8nio/n8n:latest
environment:
- DB_SQLITE_VACUUM_ON_STARTUP=true
- EXECUTIONS_DATA_PRUNE=true
- EXECUTIONS_DATA_MAX_AGE=168
volumes:
- ./n8n-data:/home/node/.n8n
DB_SQLITE_VACUUM_ON_STARTUP=true makes n8n compact the SQLite file at every boot. The trade-off, straight from the docs, is that the first startup after enabling it will be slower because VACUUM is blocking. If your instance restarts frequently and the delay bothers you, an alternative is to vacuum manually once per maintenance window and leave the flag off. Start n8n and confirm it comes up clean:
docker compose up -d n8n
docker compose logs n8n --tail 50
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:5678/healthz
A 200 from /healthz means the instance is serving again.
Step 6: Tune execution pruning so the database stops growing
VACUUM reclaims space; pruning controls how fast the database refills. n8n deletes finished executions on a schedule, but the defaults are generous for a small VPS: 14 days of history and up to 10,000 executions. For most teams, a week of history is plenty for debugging. Set EXECUTIONS_DATA_MAX_AGE=168 (168 hours is 7 days) as shown in the compose snippet above, and consider whether you need to save successful executions at all. If your workflows are monitored elsewhere, EXECUTIONS_DATA_SAVE_ON_SUCCESS=none stops the biggest source of database growth at its origin, while still saving errors for debugging. The n8n pruning guide documents these variables and their defaults; the SQLite note on that page is exactly what motivates this step. For Docker installs, the same variables also belong in your n8n Docker Compose setup notes so the next rebuild keeps them.
Step 7: Switch SQLite to WAL mode for reliability
Still on the database configuration reference: DB_SQLITE_POOL_SIZE controls whether SQLite opens the database file in rollback-journal mode (the default, value 0) or WAL mode (any value above 0). WAL mode is, in the docs’ words, “much more performant and reliable.” For a single n8n process it reduces lock contention during heavy execution bursts, which matters when queue workers or frequent cron executions hammer the database. Add it to the same environment block:
environment:
- DB_SQLITE_VACUUM_ON_STARTUP=true
- DB_SQLITE_POOL_SIZE=5
One caution: do not treat WAL mode as a substitute for PostgreSQL if you run multiple n8n workers. SQLite still has a single writer, and write contention under parallel workers produces SQLITE_BUSY (“database is locked”) errors. WAL mode improves the single-process case; scale-out needs a real database server, which is the next step.
Step 8: Add disk monitoring so this never surprises you again
The outage pattern is always the same: silent growth, then a sudden 503. Add a cheap guard. A cron job that alerts when the database volume passes 80 percent is enough:
# in root's crontab
*/30 * * * * df / | awk 'NR==2 && $5+0 > 80 {print "n8n disk usage "$5" on "$(date)}' | mail -s "Disk alert" admin@example.com
Also point an uptime monitor at the /healthz endpoint on your n8n domain. The health endpoint goes red when n8n cannot reach its database, which catches this class of failure faster than a generic homepage check. If you update n8n regularly, keep these guards in place across upgrades; the self-hosted n8n update guide reminds you that container rebuilds keep the volume but can reset your memory of which guards you had.
Step 9: Plan the PostgreSQL migration (the long-term fix)
If n8n is load-bearing for your business, SQLite is a stepping stone, not a destination. The official recommendation for production n8n is PostgreSQL, and the migration is a planned maintenance window, not a rescue operation:
- Export everything with n8n stopped:
n8n export:workflow --all --output=/home/node/.n8n/backup-workflows/and the same for credentials withn8n export:credentials --all. - Add a
postgresservice to your compose file (thepostgres:16image is a sane default) with a persistent volume and apg_isreadyhealthcheck, and make n8ndepends_onit withcondition: service_healthy. - Point n8n at it with
DB_TYPE=postgresdb,DB_POSTGRESDB_HOST=postgres,DB_POSTGRESDB_USER,DB_POSTGRESDB_PASSWORD, andDB_POSTGRESDB_DATABASE. - Start the stack, then import:
n8n import:workflow --input=/home/node/.n8n/backup-workflows/and the credentials import. - Verify workflows, credentials (re-enter any that fail decryption), and execution history before deleting the SQLite file.
The full walkthrough of the destination setup lives in the n8n PostgreSQL tutorial, which covers the compose layout and the environment variables in detail.
Troubleshooting
The first startup after enabling vacuum takes forever
This is expected, not a new problem. n8n’s docs state that VACUUM is a long-running blocking operation that increases start-up time, and on a multi-GB database the first pass can take many minutes. Watch the logs; as long as the container is running and the log shows startup progress, let it finish. If the delay is unacceptable for your restart cadence, run VACUUM manually once during a maintenance window (Step 4) and leave the environment variable off, repeating the manual run monthly.
“database is locked” (SQLITE_BUSY) errors appear under load
SQLite allows a single writer. Bursts of parallel executions, queue mode with several workers, or a busy cron schedule all contend for that one write lock, and the losers get SQLITE_BUSY. Enable WAL mode with DB_SQLITE_POOL_SIZE as in Step 7 to reduce contention for the single-process case. If you genuinely need parallel workers, stop tuning SQLite and move to PostgreSQL (Step 9); no journal mode fixes the single-writer limit.
The database is still huge after pruning ran
Pruning deletes rows, but SQLite reuses the freed space instead of returning it to the disk. This is the exact behavior n8n documents, and it is why pruning alone never shrinks the file. A single manual VACUUM (Step 4) is the fix; pruning plus periodic vacuum, or the on-startup flag, is the prevention.
n8n still returns 503 after the vacuum
If the database is compact and n8n still will not stay up, the disk problem is elsewhere. Check Docker’s own disk usage (docker system df) because build cache and unused images can fill a small VPS, inspect the binary_data directory inside the n8n volume for orphaned execution files, and check inode exhaustion with df -i. Also verify the volume mount: if the compose file changed, n8n may have booted against a fresh empty volume while your real database sits in another directory, which looks exactly like a broken instance but is just a mount mismatch. Fix the mount and restart.
FAQ
The FAQ pairs in this article’s metadata are rendered below by the theme; they cover the startup slowdown after enabling vacuum, whether VACUUM risks data loss, when to migrate to PostgreSQL, how to diagnose disk versus database, and whether VACUUM can run against a live instance.
Conclusion
The “database not ready” failure is almost never a corrupt database. It is a full disk wearing a database costume: execution rows get pruned on schedule, but SQLite holds onto the freed space until someone runs VACUUM. Measure with df and du, stop n8n, back up, vacuum, then automate the prevention with DB_SQLITE_VACUUM_ON_STARTUP=true, tighter pruning, and WAL mode. When the instance outgrows SQLite, migrate to PostgreSQL on your own schedule instead of during the next outage. Next, work through the n8n error-handling playbook to build the same systematic triage habit for every other failure class n8n throws at you.
Frequently asked questions
Will DB_SQLITE_VACUUM_ON_STARTUP=true make n8n slow to start?
Only the first startup after you enable it. n8n's docs describe VACUUM as a long-running blocking operation that increases start-up time, so a multi-GB database can take several minutes on its first pass. Later restarts are fast because there is far less bloat to compact. Many admins run VACUUM once manually during a maintenance window instead of leaving the flag on.
Do I lose workflows or credentials when I run VACUUM?
No. VACUUM rebuilds the database file to reclaim unused space; it does not delete tables or rows. Your workflows, credentials, and execution history survive intact. Still, copy database.sqlite to a backup before you start, because any disk operation on a full drive carries risk.
Why not just switch to PostgreSQL immediately?
PostgreSQL is the right long-term answer for busy instances, but the migration needs a planned maintenance window: you export all workflows and credentials, stand up a Postgres stack, and import everything back. VACUUM unblocks you in minutes, which is usually the correct first move when n8n is already down.
How do I tell whether the disk or the database is the real problem?
Check both with two commands: df -h for free disk space and du -sh on the database.sqlite file. The classic signature of this bug is a multi-GB database.sqlite on a disk with less than about 10 percent free, plus 503 responses from your reverse proxy while n8n restarts in a loop.
Can I run VACUUM while n8n is running?
Do not. VACUUM needs exclusive access to the database file, and running it against a live n8n instance can collide with active transactions. Stop the n8n container first, run the vacuum, then start n8n again. If you want a hands-off option, the DB_SQLITE_VACUUM_ON_STARTUP=true environment variable performs the same compaction at the next boot.