The useful part of most “SQLite in production” guides is the same: put the database beside the application, enable WAL for a mixed read/write workload, keep transactions short, wait briefly for the one writer, and build a real backup path. The unreliable part is the universal recipe that follows—fixed cache sizes, gigabytes of mmap, unconditional BEGIN IMMEDIATE, manual checkpoints on a timer, and synchronous=NORMAL described as free performance.
This note synthesizes the strongest advice in the linked practitioner posts, checks it against SQLite’s own documentation, and corrects the most consequential claims in the reference article. The result is deliberately smaller than a tuning cookbook: establish fit, preserve explicit durability, instrument contention, and change one mechanism only when the workload gives a reason.
The smallest safe operating model
SQLite is a strong production choice when one application deployment owns one local database file and the workload can serialize its writes without violating latency objectives. WAL usually improves that shape. It does not make SQLite a multi-writer database server.
| Dimension | Default position | Reason to change it |
|---|---|---|
| Topology | Application and writable database on the same host and a reliable local filesystem. | Use a client/server database when several hosts or services need direct write access. |
| Journal mode | Use WAL for a long-lived, mixed read/write server process. | Rollback journal may fit read-mostly databases opened and closed by short-lived processes, immutable artifacts, or files moved between machines. |
| Write admission | Set a finite busy timeout on every connection; keep write transactions short. | Add an application queue or writer limit when contention is persistent rather than bursty. |
| Transaction mode | Use BEGIN IMMEDIATE for transactions known to write, especially read-then-write sequences. | Leave read-only work deferred or in autocommit; do not reserve the writer unnecessarily. |
| Checkpointing | Keep the default automatic checkpoint until measurements show a problem. | Move checkpoints off the request path when they produce visible commit spikes or uncontrolled WAL growth. |
| Durability | Use synchronous=FULL when acknowledged commits must survive power loss. | Use NORMAL only when the accepted recovery-point objective permits recently committed transactions to roll back after a hard failure. |
| Memory tuning | Keep cache and mmap defaults initially. | Change them after workload-specific benchmarks, with the whole connection pool’s memory budget included. |
| Recovery | Make consistent backups, copy them off-host, and run restore drills. | Add asynchronous replication when the required recovery point is tighter than periodic backup can provide. |
The decisive constraint is not daily request count or database size. It is the topology of writers, the length of write transactions, and the failure modes the team is prepared to operate.
Operating position
Start with the few settings that encode correctness and lock behavior. Treat performance PRAGMAs as experiments. A configuration is production-ready only when its restore path and contention behavior have been exercised under failure.
Fit before flags
SQLite’s best server-side deployment keeps the application process and database file inside the same machine and operational boundary. That removes database network latency and a separate daemon, but it also makes the application deployment responsible for storage, locking, upgrades, backup, and recovery. SQLite’s own guidance recommends a client/server database when data is separated from the application by a network or when many computers need concurrent direct access. appropriate uses
WAL has a sharper topology limit: all processes using the writable database must be on the same host because they coordinate through shared memory. A writable WAL database on NFS or a similar network filesystem is therefore not a supported route to horizontal application scaling. WAL documentation
A good fit
One primary machine; local persistent storage; modest or bursty writes; short transactions; read-heavy endpoints; a team that values a small operational surface and can own host-level recovery.
A poor fit
Several independent writers across machines; cross-region writes; long write transactions; a fleet of services needing direct SQL; database roles and extensions; managed failover as an immediate requirement.
Database size alone is a weak decision rule. A large read-mostly database can be comfortable in SQLite; a small database with long, frequent, latency-sensitive writes can be a poor fit. “Millions of queries per day” is equally unhelpful without a concurrency distribution, query mix, transaction duration, storage profile, and tail-latency target.
The most useful practitioner arguments make this architectural case rather than promising a throughput number. Wesley Aptekar-Cassels emphasizes the absence of a separate database server and the broad range of web workloads that do not need sustained high write concurrency; Marian Posaceanu’s database-choice note frames the decision as the smallest system that honestly meets durability and deployment needs. Consider SQLite smallest useful choice
Do not hide the topology
A connection pool, multiple threads, and several processes on one host are still one database owner. A second application machine writing the same file is a different architecture. Replication can distribute copies, but it does not give ordinary SQLite multiple simultaneous primaries.
What WAL changes—and what it leaves intact
In rollback-journal mode, a writer and readers can block one another during parts of a transaction. WAL appends changed pages to a separate log, allowing readers to retain a snapshot while a writer appends newer pages. This is why WAL is usually the right starting point for a web process that reads continuously and writes intermittently. Ben Johnson’s walkthrough is a useful visual explanation of the WAL header, frames, wal-index, and reader end marks. WAL internals
The invariant remains: one write transaction at a time per database file. WAL removes most reader–writer interference; it does not permit two writers to modify the same database concurrently. A request can therefore have a fast query plan and still wait behind a transaction that is doing network calls, rendering, logging, or unrelated computation while holding the writer. transaction semantics
Keep the writer’s critical section narrow
- Resolve remote API calls, object-storage uploads, email, and expensive computation before opening the write transaction.
- Fetch only the data required for the decision; prepare values before acquiring the writer.
- Batch related writes when batching shortens total lock time, but avoid turning an endpoint into one large transaction merely to improve a synthetic throughput number.
- Finalize cursors and statements promptly. An unfinished statement can keep a transaction active longer than application code suggests.
- Separate large maintenance writes from peak request traffic or divide them into bounded units when semantics permit.
WAL is also not a promise that readers can never see SQLITE_BUSY. SQLite documents short exclusive windows during recovery and when the final connection closes and cleans up WAL state. Hynek Schlawack’s July 2026 report is a useful counterexample: many short-lived read-only processes opening and closing a WAL database can encounter rare lock errors even when application data is not being written. A non-zero timeout still matters for readers, and rollback journal may be the better fit for that unusual lifecycle. short-lived readers
Version gate: verify the library you actually ship
As of this note, SQLite 3.53.4 is the current release. More importantly for WAL deployments, SQLite documents a rare WAL-reset corruption race affecting versions likely spanning 3.7.0 through 3.51.2; it is fixed in 3.51.3 and later, with backports in 3.44.6 and 3.50.7. The race requires WAL, at least two connections, and a tightly timed write/checkpoint interaction, but production deployments should not rely on rarity when a patch exists. release history WAL-reset bug
Runtime verification
SELECT sqlite_version();
PRAGMA compile_options;
PRAGMA journal_mode;
Check from the application runtime, not only from the sqlite3 command-line binary installed on the host. Language runtimes and native packages may embed or link a different SQLite library.
Busy handling and transaction shape
With a zero busy timeout, a connection that cannot acquire a required lock can fail immediately. A finite timeout converts brief bursts of contention into waiting rather than application errors. Configure it on every connection, including background workers and administrative paths. busy_timeout
5000 milliseconds is a common starting point, not a safety constant. A five-second lock wait may be acceptable for a background task and unacceptable for an interactive endpoint. Choose the timeout against the operation’s latency budget, then record lock-wait duration and failures so the queue is visible.
Do not depend on an undocumented retry algorithm
SQLite promises that the busy timeout sleeps repeatedly until the accumulated delay reaches the configured limit. It does not promise exponential backoff. SQLite may also skip the busy handler when invoking it could contribute to a deadlock. Only one busy handler exists per connection, so a framework’s custom handler and PRAGMA busy_timeout can replace one another. C API handler rules
Why a timeout does not fix every read-then-write transaction
A deferred transaction that begins with a read owns a historical snapshot. If another connection writes before the first transaction attempts its own write, upgrading that existing read transaction may be impossible. The write can return SQLITE_BUSY rather than wait in the way developers expect. Bert Hubert gives the clearest practitioner explanation of this failure mode. read-to-write upgrade
For a transaction already known to write, BEGIN IMMEDIATE asks for the write transaction at the start. If it succeeds, the later statements do not need to upgrade a read snapshot. If another writer is active, the BEGIN IMMEDIATE itself can wait or fail. This moves contention to a predictable boundary; it does not eliminate contention.
Use IMMEDIATE
Inventory checks followed by an update; read-modify-write counters; uniqueness decisions enforced across several statements; any transaction that is semantically certain to write.
Keep deferred or autocommit
Pure reads; reporting queries; health checks; routes that only sometimes write and can be refactored to decide before opening the transaction.
“Always use BEGIN IMMEDIATE for every transaction containing a possible write” is too broad. Reserving the writer early can reduce useful concurrency when the transaction spends time reading or decides not to write. Stephen Margheim’s Rails work adds another operational lesson: the busy handler must cooperate with the language runtime. A handler that holds a global interpreter or VM lock while sleeping can damage fairness and tail latency even though the database setting is technically correct. SQLite on Rails
Finally, BEGIN EXCLUSIVE is not a stronger reader-blocking mode under WAL. SQLite documents EXCLUSIVE and IMMEDIATE as equivalent in WAL mode; the difference applies to other journal modes. transaction modes
Checkpoint policy and tail latency
A checkpoint copies committed pages from the WAL back into the main database. By default, SQLite attempts an automatic checkpoint when a commit leaves the WAL at roughly 1,000 pages. The default checkpoint is passive: it copies what it can without forcing active readers out of the way. This policy is intended to work well for most applications. automatic checkpoint
The default has one visible performance shape: most commits are cheap, while the commit that crosses the threshold may also perform checkpoint work and become slower. Moving checkpointing to another thread or process can smooth the request path, but it also makes the application responsible for scheduling, monitoring, and ensuring that checkpoints can complete.
Start with observation, not a timer
- Record commit latency and the duration and result of checkpoints.
- Track WAL bytes or frames over time, not merely whether the
-walfile exists. - Find long read transactions and streaming cursors that pin old WAL frames.
- Look for “reader gaps”—moments with no old snapshot preventing a complete reset.
- Keep enough free disk to survive a stalled checkpoint and the largest credible write burst.
Checkpoint starvation occurs when at least one reader continuously holds an older snapshot. A passive checkpoint can make progress up to that reader’s end mark, but it cannot reset the WAL while the snapshot remains relevant. Overlapping readers can therefore let the WAL grow without bound. The first remedy is usually to shorten reader lifetimes and close cursors, not to issue more aggressive checkpoints continuously.
| Mode | Operational use | Risk |
|---|---|---|
| PASSIVE | Background maintenance under normal traffic; copies available frames without waiting for readers or writers. | May not complete or reset the WAL when readers are pinned. |
| FULL | Attempt to complete a checkpoint while waiting for writers and relevant readers. | Can add waiting and should not be fired blindly from latency-sensitive paths. |
| RESTART | Complete the checkpoint and wait until no reader still uses the WAL, allowing the next writer to restart it. | More intrusive; use when the application understands the traffic window. |
| TRUNCATE | Like restart, then physically truncate the WAL to zero bytes. | Extra filesystem work; useful for controlled cleanup, not a universal steady-state policy. |
journal_size_limit is often misunderstood here. It controls how large a journal or WAL file is left on disk after a transaction or WAL reset; it is not a hard cap that stops an active WAL from growing during checkpoint starvation. journal_size_limit
Checkpoint rule
Keep automatic checkpointing until data shows either request-path spikes or WAL growth. When taking control, prefer a monitored passive checkpointer in normal traffic and reserve restart/truncate behavior for controlled conditions. Do not disable the default and replace it with an unobserved cron job.
Durability is a product decision
In WAL mode, synchronous=FULL syncs the WAL at each transaction commit. synchronous=NORMAL omits that per-commit sync and relies on checkpoint synchronization. Both modes protect database consistency under the documented model, but they do not provide the same durability.
With NORMAL, a process crash is different from a power loss or hard operating-system reset. A process crash generally leaves WAL content available to recover. A hard failure can lose recently committed WAL content that the application had already acknowledged. SQLite states this directly: transactions may roll back after power failure or hard reset. WAL durability synchronous
NORMALis a consistency-preserving durability trade. It is not “only uncommitted work can be lost.”
| Workload | Reasonable starting point | Decision test |
|---|---|---|
| Payments, orders, audit events | FULL | Can an acknowledged commit disappear after host power loss without violating a user, accounting, or legal promise? |
| Ordinary application state | FULL unless the RPO explicitly permits loss | Can the team explain and reconcile the last seconds of accepted writes after a hard failure? |
| Derived cache or rebuildable index | NORMAL may be appropriate | Is the data reconstructable and is rollback after a hard reset operationally harmless? |
| Bulk import with source retained | Temporary mode chosen by runbook | Can the import be restarted cleanly, and is the mode restored afterward? |
Replication does not automatically repair this choice. An asynchronous replica can have its own lag window, and a replicator reading WAL pages cannot upload bytes that the host loses before they become durable or before the next sync. Specify the local commit guarantee, replication lag, and backup interval separately.
Storage still matters
Durability assumes the filesystem and device honor synchronization semantics. A setting cannot compensate for an ephemeral volume, broken network filesystem locking, fake storage capacity, or a device that lies about flush completion. Persistent local storage plus tested off-host recovery is the baseline.
Cache, mmap, planner maintenance, and vacuum
The most copied SQLite tuning blocks are weakest in this section because the settings interact with database size, page size, access locality, connection count, operating system, language binding, and storage. The right question is not whether a larger number sounds “production-grade”; it is whether the change improves the real workload without creating a memory or failure-mode regression.
cache_size: count the pool
A negative cache_size is an approximate kibibyte target; a positive value is a page count. SQLite describes it as a suggested maximum per open database file, allocated on demand by the default page cache. It lasts for the current session. A pasted value of roughly 64 MiB can therefore represent about 64 MiB for each connection touching the database—not one global cache. cache_size
Before increasing it, measure cache effectiveness, database working set, process resident memory, connection-pool width, and pressure from the operating system’s own page cache. A larger cache can help repeated page access; it can also multiply memory usage without changing latency.
mmap_size: a ceiling, not a preload
mmap_size sets the maximum portion of a database accessed through memory-mapped I/O. It reserves virtual address space subject to build and platform limits; it does not guarantee that the whole database is resident in physical memory. SQLite’s mmap documentation explicitly notes that mapping can improve some reads, reduce performance in other situations, and expose operating-system-specific failure modes. Writes still require normal handling rather than becoming simple in-place pointer stores. memory-mapped I/O mmap_size
Anže Pečar’s Django benchmark is useful precisely because it resists a universal story: WAL and transaction behavior produced the largest gains in that write-heavy test, while NORMAL and mmap had comparatively small effects. That result is not a new universal law; it is evidence to benchmark the application’s own query mix. Django benchmark
PRAGMA optimize: planner maintenance
For current SQLite, PRAGMA optimize is the recommended way to decide when planner statistics need updating. SQLite recommends PRAGMA optimize=0x10002 when opening a long-lived connection, then PRAGMA optimize periodically and after schema or index changes. It is normally a no-op and bounds analysis work when needed. optimize
auto_vacuum=INCREMENTAL: space reclamation only
Incremental auto-vacuum stores metadata needed to reclaim free pages later; it does not optimize query plans or index allocation. It usually must be enabled before tables are created, or followed by a full VACUUM when changing from none. Reclamation occurs only when PRAGMA incremental_vacuum is invoked. Enable it when the database’s delete pattern and disk budget justify a reclamation schedule, not as a generic performance flag. auto_vacuum
Tuning rule
Omit cache, mmap, journal-size, temp-store, and vacuum changes from the first production baseline unless the application has a measured problem they address. Record the before/after workload, connection count, database size, storage type, p95/p99 latency, throughput, errors, and resident memory.
Backup, restore, and replication are different jobs
A database is not durable merely because the file is on a persistent volume. Durability needs an independent copy, a known recovery point, and a restore procedure that has been executed. Replication may reduce data loss and recovery time; it is not a substitute for versioned backups because replicas can reproduce application mistakes, malicious writes, or logical corruption.
Make a consistent copy
Blindly copying only the main .db file while transactions are active can produce an inconsistent or corrupt backup. In WAL mode, the main database and active -wal state form a recovery unit. SQLite documents three live-safe approaches: the online backup API, VACUUM INTO, and sqlite3_rsync for a live copy over SSH. A filesystem snapshot can also work when its consistency semantics are understood and tested, but “the volume supports snapshots” is not itself proof that an application-consistent database image is produced. backup hazards backup API VACUUM INTO
| Mechanism | Primary job | Operational note |
|---|---|---|
Online backup API / .backup | Consistent live copy | Widely exposed by language bindings and the CLI; copy the result to independent storage. |
VACUUM INTO | Compact consistent snapshot | Produces a new database; needs temporary space and should not overwrite the live file. |
sqlite3_rsync | Bandwidth-efficient live copy over SSH | Available in SQLite 3.47.0 and later; verify the shipped CLI version. |
| Litestream | Asynchronous continuous replication and point-in-time recovery | Standard mode is a background process that reads committed WAL pages and writes LTX history to a replica destination. |
| LiteFS | Live primary/replica topology with local copies | A passthrough distributed filesystem with one primary writer and asynchronous replication; materially more operational machinery. |
Litestream: usually a disaster-recovery sidecar
Litestream’s standard mode runs as a separate background process. It reads WAL pages through SQLite, packages changes into LTX files, and sends them asynchronously to a replica destination. That makes it useful for a low-recovery-point disaster-recovery path without putting a network round trip on each database commit. It also means there is a lag window to quantify and a restore workflow to test. Litestream now documents an optional VFS extension for read replicas; that is a separate mode, not the mechanism of ordinary replication. Litestream architecture tips and caveats
LiteFS: a distributed filesystem, not a PRAGMA
LiteFS intercepts filesystem operations through a passthrough filesystem, extracts transactions into LTX, maintains a single primary, and asynchronously propagates changes to replicas. It can provide local reads and failover-oriented topology, but it brings FUSE/filesystem behavior, leases or primary routing, replication lag, and split-brain concerns into the operating model. Fly’s documentation currently advises caution and regular off-site backups. LiteFS architecture LiteFS overview
Specify recovery objectives before choosing a tool
- RPO: how much acknowledged data may be lost after host or region failure?
- RTO: how long may the application remain unavailable while a database is restored and promoted?
- Retention: how far back must operators be able to recover from a bad migration, deletion, or compromise?
- Restore granularity: is a latest snapshot enough, or is point-in-time selection required?
- Independence: are copies in a different account, failure domain, and credential boundary?
Restore drill, after restoring to an isolated path
PRAGMA integrity_check;
PRAGMA foreign_key_check;
SELECT sqlite_version();
-- Then run application-level invariants and a read-only smoke test.
The VFS boundary matters
SQLite’s Virtual File System is the internal portability layer through which SQLite asks the operating system to open, read, write, sync, lock, and map files. A custom SQLite VFS can change those operations inside the database engine. A FUSE filesystem, by contrast, presents a filesystem interface at the operating-system boundary. A separate replication process can simply use ordinary SQLite and filesystem APIs. These mechanisms may solve related storage problems, but they are not synonyms. SQLite VFS
| System | Boundary | Accurate description |
|---|---|---|
| SQLite VFS | Inside SQLite’s OS abstraction | Implements SQLite file I/O, locking, shared-memory, timing, randomness, and related primitives. |
| Litestream standard replication | Separate process beside SQLite | Reads committed WAL pages and asynchronously writes LTX history to remote storage. |
| Litestream VFS extension | SQLite extension/VFS mode | Optional read-replica and hydration functionality, distinct from the standard sidecar mode. |
| LiteFS | Passthrough/FUSE filesystem | Intercepts filesystem calls, captures transactions, and replicates a single-primary database to nodes. |
The distinction is operationally useful. A sidecar can fail, lag, or lose credentials while the primary database continues serving. A FUSE layer can affect every database filesystem call. A custom VFS can alter SQLite’s locking and sync semantics. Those are different blast radii and require different health checks.
Cloud deployment does not make a “VFS-based replication tool” mandatory. It makes durable storage and a recovery design mandatory. A single VM with a persistent volume and tested off-host backup can be valid. A container using only an ephemeral writable layer is not. The mechanism follows the recovery objectives and platform constraints.
A production baseline with fewer knobs
This baseline encodes topology, referential integrity, lock waiting, durability, and planner maintenance. It intentionally omits cache, mmap, journal-size, auto-vacuum, and custom checkpoint values.
SQLite production baseline
-- Deployment or migration step: verify the embedded library.
SELECT sqlite_version();
-- Database-level journal choice. Check that the returned value is "wal".
PRAGMA journal_mode = WAL;
-- Every connection (web, jobs, console, migrations).
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000; -- starting point, not a law
PRAGMA synchronous = FULL; -- choose NORMAL only by explicit RPO
-- Long-lived connections: planner-statistics maintenance.
PRAGMA optimize = 0x10002;
-- Also run PRAGMA optimize periodically and after schema/index changes.
-- For a transaction known to write:
BEGIN IMMEDIATE;
-- Read and write only. No network I/O or unrelated computation.
COMMIT;
-- Only after instrumentation justifies an owned checkpoint policy:
PRAGMA wal_checkpoint(PASSIVE);
Implementation notes
journal_mode=WALpersists in the database header, but the command can fail to change the mode. Assert that it returnswal.foreign_keys, busy handling, cache, mmap, and other behavior can be connection-local. Apply required settings in the connection initialization hook, not once in an interactive shell.- Some frameworks already install a custom busy handler or select transaction modes. Understand the adapter before layering another PRAGMA on top.
- Use a pool no larger than the workload needs. Extra connections do not create extra writer capacity and can increase memory and lock competition.
- Keep migrations and maintenance tools on the same SQLite version policy as the application, especially when they can write or checkpoint.
- Run the database on persistent local storage with enough free-space headroom for WAL growth, backup staging, and schema changes.
The baseline is intentionally conservative. In applications where losing the last acknowledged writes after a hard reset is acceptable, changing to NORMAL may reduce synchronization work. In applications where request-path checkpoints are measurable, an owned background policy may flatten tail latency. Those are explicit decisions backed by tests, not prerequisites for calling SQLite “production.”
What to measure and what to break on purpose
Average query latency is not enough. SQLite production incidents usually appear in the tails: a lock upgrade that does not wait, a long reader pinning the WAL, a checkpoint on an unlucky commit, a full disk, an asynchronous replica behind the primary, or a restore script that was never exercised.
| Signal | Why it matters | Useful breakdown |
|---|---|---|
| Busy outcomes | Shows lock contention and failed admission. | Count by extended result code, endpoint/job, transaction mode, and timeout. |
| Lock-wait time | Separates query time from time queued for the writer. | p50/p95/p99 and maximum, per operation class. |
| Write-transaction duration | Directly consumes the single-writer budget. | Time from begin to commit, including application work. |
| WAL size / frame count | Reveals checkpoint lag and pinned readers. | Rate of growth, peak, time above threshold. |
| Checkpoint result | Shows whether frames were copied and whether work completed. | Duration, busy status, log frames, checkpointed frames. |
| Long readers | Old snapshots can prevent WAL reset. | Cursor/transaction age and call site. |
| Process memory | Cache and mmap experiments can multiply across connections. | RSS, virtual size, pool width, database count. |
| Recovery state | Backups are useful only when current and restorable. | Last successful copy, replication lag, oldest recovery point, last restore drill. |
| Runtime version | Embedded SQLite can differ from host tools. | sqlite_version(), compile options, binding/package version. |
Failure drills
- Kill only the application process during a write, restart it, and verify invariants.
- Test the documented hard-failure assumption in an isolated environment; compare
FULLandNORMALagainst the accepted RPO. - Hold a read transaction open while generating writes; observe WAL growth and checkpoint results.
- Run concurrent read-then-write transactions and confirm that known writers use the intended transaction mode.
- Fill a test volume near capacity during a write burst and backup operation; verify alerting and safe failure.
- Stop replication, let lag accumulate, recover it, then restore to a selected point in a clean directory.
- Restore the newest backup without access to the primary host, using only documented credentials and artifacts.
- Upgrade the application’s SQLite library and run integrity, foreign-key, migration, and concurrency tests before production rollout.
Benchmark with the production schema, indexes, statement mix, payload sizes, pool width, transaction boundaries, storage, and runtime. Anže Pečar and Shivek Khurana provide useful examples of workload-specific comparisons, but their numbers should be treated as experiment designs rather than capacity promises. Django benchmark real-world benchmark
Corrections to the reference article
The Micrologics article is a useful map of the topics that matter—WAL, busy handling, transaction modes, memory, checkpoints, and replication. Its strongest weakness is presenting workload-dependent settings as universal production rules and blurring several failure semantics. The table below preserves the direction while correcting the claims that could change an operational decision. reference article
| Article position | More accurate version | Operational consequence |
|---|---|---|
NORMAL only risks uncommitted work | Database consistency is retained, but recently committed transactions can roll back after power loss or hard reset. | Choose it through an explicit RPO, not as a free performance flag. |
| Busy timeout uses exponential backoff | SQLite promises repeated sleeps up to a cumulative timeout; the exact strategy is not a stable API contract, and the handler can be bypassed to avoid deadlock. | Do not tune latency around an assumed exponential schedule. Check runtime-specific handler behavior. |
| Every writing transaction should always begin IMMEDIATE | Use IMMEDIATE for transactions known to write, especially read-then-write. Keep read-only transactions deferred/autocommit and minimize time after acquiring the writer. | Avoid reserving the single writer for work that may never write. |
| EXCLUSIVE blocks readers in WAL | SQLite documents EXCLUSIVE and IMMEDIATE as equivalent in WAL mode. EXCLUSIVE blocks readers in other journal modes. | Do not expect a stronger WAL lock from the transaction keyword. |
| Manual scheduled checkpoints are a production requirement | The default passive auto-checkpoint at roughly 1,000 pages works for most applications. Take control when measurements show commit spikes or WAL growth. | A manual policy creates a new monitored subsystem; it should solve observed behavior. |
journal_size_limit prevents indefinite WAL growth | It truncates the file after commit/reset conditions; it is not a live hard cap during checkpoint starvation. | Monitor long readers, checkpoint completion, free space, and WAL growth directly. |
| Allocate about 64 MiB of cache | cache_size is a suggested session-local maximum per open database, allocated on demand and multiplied across relevant connections. | Budget the complete pool and benchmark cache effectiveness before increasing it. |
| Map 1–2 GiB for faster reads | mmap_size is an optional maximum mapping. It is not physical preload, is platform/build constrained, and can help or regress performance. | Measure on the deployed OS, binding, schema, and query mix. |
| Incremental auto-vacuum optimizes plans and index allocation | It enables later incremental file-space reclamation and generally must be configured at database creation or followed by VACUUM. PRAGMA optimize is for planner statistics. | Do not change the file format for a query-planning problem. |
| Litestream and LiteFS are VFS layers | Standard Litestream is a separate asynchronous replication process; it also offers an optional VFS extension. LiteFS is a passthrough/FUSE distributed filesystem. | Model their distinct failure domains, health checks, and restore paths. |
| Cloud deployment makes VFS replication mandatory | Cloud deployment requires persistent storage and recovery designed to RPO/RTO. Replication is one mechanism, not a universal architecture. | A persistent single primary plus off-host tested backups can be valid; an ephemeral container layer is not. |
| General throughput and size thresholds decide fit | Topology, concurrent write demand, transaction duration, latency objectives, and recovery requirements dominate. | Benchmark the actual application and migrate when the operational boundary changes. |
The article’s recommendation to enable foreign keys, use WAL for concurrent server workloads, add busy handling, keep transactions explicit, and plan replication or backup is directionally sound. The corrected version is less exciting because it refuses universal constants. It is also safer to operate.
When PostgreSQL becomes the smaller system
SQLite is operationally small while one deployment owns the database. PostgreSQL often becomes operationally smaller once the application needs the guarantees and coordination that a database server already provides. The migration trigger is usually architectural before it is raw capacity.
Strong migration pressure
- Several application hosts or services need direct concurrent write access.
- Write contention is persistent, and queueing the single writer violates p95/p99 latency or job deadlines.
- Long transactions are a real business requirement rather than an implementation accident.
- Database roles, independent credentials, row-level security, extensions, logical replication, or managed read replicas are needed.
- The database must remain available while an application node is replaced, isolated, or deployed independently.
- Cross-region writes or a managed high-availability control plane are part of the current system—not a speculative future.
- Operations needs database-native observability, online administrative tooling, and a familiar managed-service ecosystem more than it needs an embedded file.
Do not migrate because SQLite feels unserious, and do not stay because a synthetic benchmark reports a high transaction rate. Migrate when the topology and operational guarantees make the server database the more direct implementation. Posaceanu’s note states the boundary well: the best choice is the one whose failure modes the team is willing to own. SQLite vs PostgreSQL
Decision rule
SQLite is simplest when the database is part of one application deployment. PostgreSQL is often simplest when the database becomes shared infrastructure.
Related reading, with the useful part identified
These posts are valuable for operational experience, diagrams, benchmark design, and framework-specific behavior. They should not override SQLite’s documentation for transaction, durability, locking, or file-format semantics.
SQLite in Production: Optimizing WAL Mode, Concurrency, and VFS Layers
Reference: Useful topic map and readable introduction. Use the correction table above before adopting its durability, checkpoint, mmap, cache, auto-vacuum, or replication claims.
SQLite on Rails: The how and why of optimal performance
Rails: Strong treatment of contention, immediate transactions, busy handlers, language-runtime scheduling, and tail latency. Translate the mechanisms rather than copying Rails-specific defaults.
What to do about SQLITE_BUSY errors despite setting a timeout
Transactions: The clearest explanation of deferred read-to-write upgrade failure and why known writers should acquire the write transaction at the start.
Django SQLite Production Config
Django: Practical adapter configuration and a useful warning that IMMEDIATE is a correctness/contention choice, not a universal performance boost.
Django SQLite Benchmark
Benchmark: Shows how WAL and transaction mode dominated one write-heavy workload while synchronous and mmap changes had small effects. Good evidence against cargo-cult tuning.
Gotchas with SQLite in Production
Operations: Compact coverage of configuration, ephemeral filesystems, single-writer behavior, and checkpointing. Notably describes the power-loss trade in NORMAL mode.
What you need to know about SQLite
Rails 8: Broad, balanced overview of SQLite’s production fit and the Rails ecosystem’s integration work, including limitations and durability choices.
Optimizing SQLite for servers
Configuration: Comprehensive tour of server-oriented settings. Use its values as hypotheses; validate current SQLite semantics and benchmark the deployed workload.
How SQLite Scales Read Concurrency
Internals: Detailed mental model for WAL frames, reader snapshots, checkpoints, and the wal-index. Useful background for interpreting metrics and lock behavior.
Consider SQLite
Architecture: Strong argument for evaluating SQLite by actual write demand and operational simplicity rather than by reflex. Capacity examples remain workload-specific.
I’m All-In on Server-Side SQLite
Litestream: Influential operational case for local reads plus continuous WAL replication. Read alongside current Litestream documentation because architecture and formats have evolved.
SQLite WAL Mode Can Lock Short-Lived Readers
Edge case: A 2026 production report showing that WAL is not automatically best for databases repeatedly opened and closed by many read-only processes.
SQLite in Production — A Real-World Benchmark
Benchmark: Useful attempt to replace headline throughput claims with a concrete schema and workload. Keep the environment and transaction mix attached to every conclusion.
SQLite, PostgreSQL, and the smallest useful database choice
Decision: Concise framing for the architectural boundary: SQLite while the deployment remains local and controlled; PostgreSQL when coordination becomes shared infrastructure.
Sources
The source hierarchy is intentional. SQLite documentation defines the engine’s semantics. Litestream and LiteFS documentation define their current mechanisms and caveats. Practitioner posts contribute experience, benchmark methods, and framework behavior.
Topology, local storage, network separation, concurrent writers, and client/server decision boundary.
Reader/writer concurrency, same-host requirement, automatic checkpoints, checkpoint starvation, durability, busy cases, and the WAL-reset bug.
Single-writer invariant, read-to-write upgrades, DEFERRED/IMMEDIATE/EXCLUSIVE semantics, and unfinished statements.
PRAGMA busy_timeout and busy handler API
Per-connection handler, cumulative waiting, replacement behavior, and cases where the handler is not invoked.
Synchronization levels and the distinction between consistency and durability.
PRAGMA cache_size and Memory-Mapped I/O
Suggested per-open-database cache limits, on-demand allocation, mmap benefits, regressions, and platform caveats.
Post-commit/reset file retention limit, not an active cap against checkpoint starvation.
PRAGMA auto_vacuum and PRAGMA optimize
Space reclamation mechanics, database-creation constraints, and current planner-statistics maintenance recommendations.
Online Backup API, VACUUM INTO, and sqlite3_rsync
Supported mechanisms for consistent live copies.
How To Corrupt An SQLite Database File
Backup hazards, journal pairing, locking and filesystem failure modes, and WAL race context.
The SQLite OS Interface or VFS
Definition and responsibilities of SQLite’s Virtual File System layer.
Current release verification: 3.53.4 on 24 July 2026; WAL-reset fixes and backports.
Background-process architecture, WAL-to-LTX conversion, checkpoints, snapshots, retention, restore granularity, and optional VFS read replicas.
Busy timeout, foreign keys, asynchronous loss window, high-write workloads, and operational caveats.
Passthrough filesystem interception, LTX capture, replication position, single primary, and leases.
LiteFS FAQ and overview
Asynchronous replication window, throughput caveats, backup responsibility, support posture, and deployment warnings.
Stephen Margheim — SQLite on Rails
Framework/runtime-specific contention, transaction, and busy-handler analysis.
Bert Hubert — SQLITE_BUSY despite timeout
Read-to-write upgrades and transaction-mode explanation.
Anže Pečar — Django SQLite Production Config and benchmark
Concrete framework configuration and comparative workload evidence.
Joy of Rails — What you need to know about SQLite
Broad production-fit overview and modern Rails integration.
Sylvain Kerkour — Optimizing SQLite for servers
Detailed configuration survey to use as a benchmark agenda, not a universal constants file.
Ben Johnson — How SQLite Scales Read Concurrency
WAL internals and readable mental models.
Wesley Aptekar-Cassels — Consider SQLite
Architectural and operational argument for server-side SQLite.
Hynek Schlawack — WAL can lock short-lived readers
Production edge case and connection-lifecycle caution.