We run a high-density infrastructure at Hostna where we see how database choices dictate workflow reliability. For n8n instances, the shift from SQLite to PostgreSQL becomes mandatory when execution volume exceeds roughly 20 concurrent write operations per second or when using Queue Mode with multiple worker processes. While SQLite is an excellent choice for low-volume automation, its architectural reliance on database-level locking creates a bottleneck as your workflow complexity grows.
| Symptom | Probable Cause | Fix |
|---|---|---|
SQLITE_BUSY: database is locked |
Concurrent writes exceeding SQLite lock limits |
Enable WAL mode or migrate to PostgreSQL |
| High CPU during database writes | Synchronous disk commits on slow storage | Move to NVMe-backed storage on our platform |
| Workers failing in Queue Mode | SQLite used with multiple containers |
Switch DB_TYPE to postgresdb in docker-compose.yml |
The Mechanism of SQLite Locking Contention
By default, SQLite uses a journaling mode that requires an exclusive lock on the entire database file during write operations. This means that if one workflow is writing an execution log, all other workflows must wait to read or write. As you scale an automated workflow environment, this wait time—known as lock contention—increases exponentially. You can check your current journal mode by running sqlite3 /home/node/.n8n/database.sqlite "PRAGMA journal_mode;". If it returns delete, you are using the most restrictive locking mechanism.
Switching to Write-ahead logging (WAL) allows multiple readers and one writer to operate simultaneously. However, even with WAL, SQLite cannot handle true row-level concurrency. In high-volume scenarios where hundreds of workflows trigger simultaneously, the single-writer limitation leads to the dreaded SQLITE_BUSY error. This is particularly evident when using n8n with heavy JSON payloads, as the database must serialize and write large strings to a single file on the plan you are on.
Quantitative Thresholds for Migration
Technical analysis indicates that a single SQLite file begins to show significant latency when the database size exceeds 5 GB or when the executions_entity table contains more than 100,000 rows. At this point, even simple SELECT queries for the the dashboard become sluggish because SQLite lacks the sophisticated query planning and indexing capabilities found in PostgreSQL.
If your instance processes more than 5,000 executions per day, the overhead of maintaining the SQLite database—including the VACUUM command required to reclaim space—can consume significant IOPS. To measure the IOPS of the block device on our German-hosted stack, you can use the following fio command:
fio --name=random-write --ioengine=libaio --rw=randwrite --bs=4k --size=128m --numjobs=1 --iodepth=1 --runtime=60 --time_based --end_fsync=1
If your 4k random write performance is low, the synchronous nature of SQLite writes will further degrade the execution speed of every node in your workflow.
PostgreSQL and the Advantages of Row-Level Concurrency
When you transition to PostgreSQL, you move from file-level locking to row-level locking. This allows n8n to update multiple execution records in parallel without blocking. Furthermore, PostgreSQL handles JSONB data types more efficiently than SQLite, which stores JSON as plain text. This leads to better performance when searching through execution data or using the n8n API to retrieve specific results.
For a PostgreSQL database supporting n8n, you should set shared_buffers to roughly one quarter of your total RAM. This ensures that the most frequently accessed indexes are kept in memory. On high-performance virtual machines, you can monitor database activity using pg_stat_activity to identify long-running queries that might be slowing down your workflows. Unlike SQLite, PostgreSQL can also be tuned for high-concurrency by adjusting the max_connections parameter to match your worker count.
Impact of n8n Queue Mode
The most compelling reason to switch to PostgreSQL is the use of n8n Queue Mode. In this architecture, a Redis instance acts as a message broker, and multiple worker processes handle the actual execution. SQLite is structurally incompatible with this setup because multiple worker processes running in different containers or threads cannot safely share a single file-based lock over a network mount. Attempting to do so often results in database corruption.
When you run n8n in Queue Mode, each worker requires a stable connection to a centralized database. PostgreSQL is designed for this networked architecture. At Hostna, we focus on providing the underlying NVMe storage performance required for these high-throughput scenarios, ensuring that the database does not become the weak link in your automation chain. By moving the database to a separate service or a dedicated CPU resources instance, you decouple the execution logic from the data storage layer, allowing for much higher reliability.
Tuning PostgreSQL for High Execution Volume
Once you have migrated, you must ensure your PostgreSQL configuration is optimized. A common mistake is leaving the autovacuum settings at their defaults. With n8n constantly inserting and deleting execution records, the executions_entity table can suffer from bloat. You should adjust autovacuum_vacuum_scale_factor to 0.05 to ensure that the database reclaims space more frequently.
Additionally, pay attention to the memory_limit of your containers. A PHP-FPM pool—or in this case, a Node.js process—with pm.max_children = 10 and a 512 MB memory_limit can claim 5 GB in the worst case, which is why a 4 GB machine that looks idle can still be killed by the OOM killer during one traffic spike. For n8n, the memory consumption is tied to the size of the data being processed in each node. Ensure your GDPR-compliant data handling workflows are not loading massive datasets into memory simultaneously without sufficient swap space.
Diagnostic Commands for Performance Monitoring
To identify if your database is the bottleneck, use iostat -x 1 to check the %util of your disk. If the disk utilization is constantly high while n8n is idle, it is likely that PostgreSQL is performing heavy background maintenance or SQLite is struggling with a large journal file. You can also use the n8n CLI to export and prune old executions, which reduces the load on the database:
n8n export:execution --all --output=/backups/executions.json
n8n cleanup:executions --days-to-keep=7
Regular maintenance is part of the contractual framework we provide to ensure your environment remains stable. For more detailed information on database schema management, refer to the official n8n database configuration documentation.
If you find that your database is still struggling despite these optimizations, it may be time to review the service level commitments and consider upgrading to a plan with more dedicated resources. Our infrastructure team is always available via the control panel to help you analyze resource usage and suggest the most effective path forward for your specific workload.
Conclusion
Choosing between SQLite and PostgreSQL for n8n is a matter of scale. For single-user instances with low execution frequency, SQLite with WAL mode enabled is sufficient. However, for any production environment requiring high concurrency, PostgreSQL is the only resilient choice. It provides the row-level locking, JSONB efficiency, and network compatibility required for n8n‘s Queue Mode. Monitor your IOPS, tune your shared_buffers, and keep your execution history pruned to maintain peak performance on your account.
This article was drafted with AI assistance from Hostna’s own research pipeline and published without individual review. Tell us if anything here is wrong.
