There is a particular kind of incident that looks impossible at first.
An application uses SQLite. The database file lives on a mounted volume. The application passes its tests. Reads work. Writes work. WAL mode is enabled. The database even survives restarts for weeks.
Then a second machine starts using the same file.
Nothing fails immediately. There is no dramatic error saying “this database is unsafe.” Instead, the application begins to show stale reads, intermittent SQLITE_BUSY errors, failed checkpoints, or a database that passes normal health checks but cannot be opened after an unlucky interruption.
The team usually investigates the mount, changes cache settings, increases timeouts, or disables WAL. Sometimes that reduces the symptoms. It does not necessarily make the design correct.
The important question is not whether SQLite can open a file on a network filesystem. It often can. The important question is whether every process that participates in SQLite’s locking, journaling, syncing, and shared-memory protocols sees the same events in the same order.
That is a much stronger requirement.
This article examines one narrow deployment problem: using SQLite WAL mode when the database path is backed by NFS, SMB, a distributed filesystem, or a volume that looks local to several hosts. The goal is not to repeat “SQLite does not work on NFS.” The useful part is understanding exactly what breaks, why rollback mode changes the failure shape rather than eliminating the risk, and which architectures preserve SQLite’s strengths without asking a filesystem to behave like a database server.
The short version
SQLite’s WAL implementation has a hard locality requirement. All processes using the database must be on the same host because readers use a shared-memory wal-index stored beside the database. The WAL file itself is not the only coordination mechanism. A -shm file is part of the read path.
That creates three separate contracts:
Locking: processes must agree about who owns SQLite’s locks.
Ordering and durability:
fsync()or its platform equivalent must mean that earlier writes are safely ordered before later writes.Shared memory visibility: processes must see a coherent wal-index and its lock state.
A network filesystem can violate any of these contracts. WAL makes the third one unavoidable. Rollback mode avoids the WAL shared-memory index, but it still depends on reliable file locks and durable ordered writes. SQLite’s own guidance recommends a client/server database when applications and data are separated by a network. Another safe pattern is to keep SQLite and all SQLite processes on the storage host and expose a request API instead of the file.
The practical rule is simple:
Put the SQLite engine on the same host as the database file. If clients are remote, make the network boundary an application protocol, not a filesystem mount.
The rest of the article explains why.
What WAL actually changes
SQLite’s default transaction mechanism is the rollback journal. Before changing a database page, SQLite copies the original page into a journal file. It can then update the database file. If the transaction is interrupted, SQLite uses the old pages in the journal to restore the original state.
WAL reverses the direction of that operation. SQLite leaves the original database pages in place and appends changed pages to a separate write-ahead log. A transaction becomes committed when a commit record is appended to the WAL. The main database file may remain unchanged for some time. [1]
This separation is the reason WAL improves concurrency. A reader can continue reading the stable database file while a writer appends new pages to the WAL. The reader sees a snapshot. It does not need to wait for the writer to copy pages into the main file.
WAL introduces a third operation in addition to reads and writes: checkpointing. A checkpoint copies committed pages from the WAL into the main database file. SQLite normally performs automatic checkpoints when the WAL reaches a configured number of pages, with the default historically being 1,000 pages unless changed at compile time or by the application. [1]
That means WAL is not just “a different journal file.” It is a coordination protocol involving at least these files:
app.db The main database
app.db-wal The append-only write-ahead log
app.db-shm The shared-memory wal-index and its locks
The -shm file is the detail that changes the network filesystem discussion.
The wal-index is not optional bookkeeping
A WAL reader needs to answer a basic question for every database page it wants to read:
Is there a newer version of this page in the WAL, and if so, which frame contains it?
The reader could scan the WAL from the beginning for every page lookup. That would be correct but increasingly expensive as the WAL grows. SQLite instead maintains a data structure called the wal-index. It maps database page numbers to locations in the WAL and also records information needed to coordinate readers, writers, and checkpoints.
SQLite stores this index in shared memory. On common platforms, that shared memory is backed by the app.db-shm file. The exact VFS implementation can vary, but the architectural requirement does not: all participating processes need access to the same coherent wal-index and its lock regions. [1]
This is why the statement “the WAL file is on NFS” is incomplete. The problem is not only concurrent appends to app.db-wal. A second process also needs to observe the same shared-memory state as the first process.
Consider two application processes on two hosts:
Host A Host B
------- -------
SQLite connection SQLite connection
| |
+-- /mnt/db/app.db ---------------+
+-- /mnt/db/app.db-wal -----------+
+-- /mnt/db/app.db-shm -----------+
NFS or SMB
Both hosts may be able to open all three paths. That does not prove that the design satisfies SQLite’s assumptions. The operating systems have different page caches. The network filesystem has its own cache and coherence rules. Memory mappings may be implemented through client-side caching and remote operations. Lock requests may be tracked by a separate protocol. A write that appears complete to one client may not yet be visible to another client in the way the SQLite VFS expects.
SQLite’s documentation states the consequence directly: all processes using a WAL database must be on the same host, and WAL does not work over a network filesystem because the processes on separate machines cannot share the required memory. [1]
The -shm file is therefore not a disposable cache that can safely be regenerated by each host independently. It is a coordination structure. Treating it as an ordinary replicated file misses the point.
A reader snapshot depends on shared state
The WAL reader algorithm is easier to understand if we follow one read transaction.
When the reader starts, it records the location of the last valid commit record in the WAL. SQLite documentation calls this its end mark. The reader must continue using that same end mark for the duration of its transaction so that it sees one consistent snapshot. [1]
When the reader asks for page 42, it consults the wal-index. If page 42 was changed in a WAL frame before the reader’s end mark, the reader uses the newest eligible frame. Otherwise, it reads page 42 from the main database file.
This works when all processes agree about:
which WAL frames are valid
where the reader’s end mark is
which frame is the latest eligible copy of a page
which readers are still using older frames
how far a checkpoint may safely advance
The data in the WAL is not enough by itself. The wal-index makes the read path fast and provides coordination metadata. If one host sees a different index from another host, the two readers can make different decisions about the same database state.
That is the subtle failure mode. A network filesystem may successfully transfer every file byte and still fail to provide the process coordination SQLite needs. Database correctness depends on more than eventual file contents.
Checkpointing turns readers into participants
A checkpoint does not simply copy the whole WAL into the database whenever convenient. It must account for active readers.
Suppose a reader began when the WAL ended at frame 100. A writer then commits frames 101 through 110. The checkpointer may copy some of those frames into the main database, but it cannot overwrite data in a way that invalidates the reader’s snapshot. It stops at the oldest point required by active readers and remembers its progress in the wal-index. [1]
This is why a long-running read transaction can keep a WAL file from shrinking. The checkpoint cannot move past the reader’s end mark. If all processes are on one host and use the same shared-memory region, the checkpointer can reason about those readers.
Across hosts, the failure is not limited to performance. A checkpointer needs a reliable view of which readers exist and which lock state they hold. If a remote client’s lock or shared-memory update is delayed, cached, lost, or interpreted differently, the checkpointer can stop too early, fail to coordinate, or operate with an incomplete view of active readers.
A growing WAL is not proof of corruption. It is often caused by a reader that remains open for too long. But on a network filesystem it is also a useful signal that you should inspect the deployment boundary rather than only tuning the checkpoint threshold.
Useful metrics include:
WAL size over time
checkpoint mode and result codes
the number and duration of read transactions
SQLITE_BUSYandSQLITE_BUSY_SNAPSHOTerrorsthe host and process identity of every database connection
filesystem errors during sync, lock, and shared-memory operations
Do not interpret a smaller WAL as proof that the system is safe. A broken coordination mechanism can appear healthy when traffic is low.
Why local testing lies
Network filesystem failures are unusually hard to reproduce because they are often timing-dependent.
A single host using a mounted directory may not exercise the distributed part of the filesystem at all. The mount may be local to a storage service but still present a shared path to several hosts only in production. A test with one writer may never create the interleaving that exposes the issue. A test that runs for five minutes may not include a checkpoint racing with a long-lived reader. A clean shutdown may hide what an interrupted process leaves behind.
The most dangerous result is not an immediate failure. It is a successful test that establishes false confidence.
SQLite’s own network guidance warns against using early testing success as evidence that a remote database file is reliable. It describes the network filesystem problem as one of correctness and durability, not merely speed. [2]
A good test must vary at least these dimensions:
more than one host
more than one writer-capable process
readers that remain open across commits
forced process termination during writes and checkpoints
network interruptions and remounts
different client mount options
actual recovery and integrity checks after failure
Even then, a passing test is not a proof of compatibility. Filesystem semantics are part of the system contract. If the contract is weaker than the database requires, testing only shows that one set of timings happened not to fail.
The three contracts that matter
It helps to separate the problem into three independent contracts instead of saying vaguely that “NFS is bad for databases.”
1. Lock ownership must be globally consistent
SQLite uses locks to coordinate readers, writers, and checkpoints. In rollback mode, the lock state includes concepts such as SHARED, RESERVED, PENDING, and EXCLUSIVE. Multiple readers can hold SHARED locks. A writer eventually needs an EXCLUSIVE lock to update the database file. [3]
In WAL mode, SQLite uses database file locks together with WAL-specific locks. Only one writer can append to the WAL at a time, even though readers can run concurrently. The WAL documentation describes these locks as part of the single-writer design. [1]
If two hosts disagree about a lock, both may believe they can perform an operation that should be exclusive. That is a direct path to corruption. If the filesystem is overly conservative, the same workload may instead produce excessive SQLITE_BUSY errors. Both outcomes are possible when the lock implementation does not match SQLite’s assumptions.
SQLite’s locking documentation says that Unix builds rely on POSIX advisory locks and that SQLite assumes those system calls work as advertised. It specifically warns that advisory locking is buggy or unimplemented on many NFS implementations and that network filesystems under Windows have also had locking problems. [3]
2. Sync must establish the ordering SQLite expects
Database recovery algorithms rely on write ordering. A journal or WAL record must be safely persisted before SQLite relies on it to recover from a crash. A database page must be synced before a journal or WAL can be reset in ways that would discard the recovery information.
fsync() is the operating system interface SQLite uses on Unix to request that data be flushed. Windows uses FlushFileBuffers(). SQLite assumes those services do what they claim. [3]
A network filesystem adds another machine, another cache, and another protocol between the application and the storage device. A successful client-side call may require a remote acknowledgement, but the exact durability and ordering guarantees depend on the filesystem and its configuration. Delayed writes, server failover, client caching, and reconnect behavior all matter.
This is why a fast benchmark is not a durability test. Low latency can mean the client acknowledged a write before the remote storage made the required guarantee. The database cannot compensate for a weaker sync contract at the VFS boundary.
3. Shared memory must actually be shared
WAL adds the wal-index requirement. Processes on one host can map the same shared-memory region through the operating system. Processes on different hosts cannot share physical memory. A network filesystem can emulate a file that multiple hosts open, but it cannot turn ordinary file mappings into the same local shared-memory behavior that WAL expects.
A distributed filesystem might replicate file contents. That is not the same as providing a coherent shared-memory object with the lock and visibility semantics expected by SQLite’s WAL implementation.
This contract is the reason changing mount options cannot make a multi-host WAL deployment generally safe. You can improve caching behavior. You cannot remove the architecture’s shared-memory requirement by adding a flag to the mount.
Why switching back to rollback mode is only a partial answer
When a team discovers that WAL is unsupported on its network volume, the first proposed fix is often:
PRAGMA journal_mode=DELETE;
That removes the WAL and -shm files, so it removes the WAL-specific shared-memory problem. It does not make the network filesystem local. SQLite still has to coordinate access to the main database and rollback journal. It still depends on locks. It still depends on durable ordered writes.
Rollback mode also changes concurrency. A writer must prevent conflicting readers while it changes database pages. The rollback journal protects the original pages until the update is safely committed. Readers and writers cannot enjoy the same parallelism as WAL readers and writers.
SQLite’s network guidance describes rollback mode as a possible option for some remote-file situations, but with a limited concurrency model. It also says that SQLite is not tested across network scenarios in a way that can guarantee every network filesystem implementation. [2]
There are environments where a single process uses rollback mode on a remote file and the risk is acceptable. That is a narrower claim than “rollback mode makes NFS safe.” The design should specify the exact access pattern:
Is there only one process at a time?
Are all clients prevented from opening the file concurrently?
Does the filesystem provide reliable advisory locking?
Are sync and failover guarantees documented by the storage provider?
Is losing or rebuilding the database acceptable?
If the answer to the first two questions is no, rollback mode is not a complete repair.
The safe architecture: keep the engine beside the file
The most robust way to use SQLite with remote clients is to move the network boundary up one layer.
Client A ----\
Client B -----+---- application API ---- SQLite process ---- local disk
Client C ----/
The SQLite library and the database file stay on the same host. Remote clients send requests over HTTP, gRPC, a message queue, or another application protocol. The application process turns those requests into SQL transactions.
This architecture has two important properties.
First, SQLite’s file coordination remains local. The WAL index is shared by local processes or threads. Locks use the host operating system. Sync calls target the local filesystem that actually stores the file.
Second, the network carries logical requests and results instead of database page traffic. SQLite’s own network guidance makes this point: when data is separated from an application by a network, the database engine should sit near the data and filter high-volume file traffic into lower-volume query and update traffic. [2]
The API adds application work. You need authentication, authorization, connection management, request limits, transaction boundaries, retries, and observability. Those are real costs. They are usually easier to reason about than asking a general-purpose filesystem to provide a distributed database protocol.
A small proxy can be enough for an internal tool. A production service may need a proper client/server database once it has high write concurrency, multiple replicas, operational failover requirements, or complex analytical workloads. The key is to make that decision at the database layer rather than accidentally creating a distributed database by mounting a file.
A useful variation: one SQLite owner with local replicas
Some systems want SQLite’s local performance and simple file format but need copies on other machines. In that case, replication must understand SQLite transactions rather than blindly replicate file blocks.
A replication layer can run on the database host, observe committed page changes, and ship transaction-level changes to replicas. The replicas can then apply those changes under SQLite’s locking rules. LiteFS is one example of a system that discusses this design. Its technical explanation shows how rollback journal and WAL transactions can be recognized as changed page sets and converted into a replication format. [4]
The important distinction is ownership. There is one writer or one authority for a given database. Other machines do not independently open the same WAL through a shared mount. They receive replicated state through a protocol designed for that purpose.
This is not the same as placing app.db, app.db-wal, and app.db-shm on a replicated filesystem and hoping every node sees a valid combined state. Replication systems can preserve transaction boundaries, ordering, checksums, and ownership rules. A filesystem generally provides files and directory operations, not SQLite-aware transaction replication.
You still need to understand the replication system’s consistency model. Some systems are primary/replica. Some permit reads from replicas but route writes to one node. Some provide eventual consistency. Those choices should be explicit.
What about containers and shared volumes?
Containers make this problem easy to misclassify.
Two containers on the same physical machine may share a local volume. If they use the same host kernel and the filesystem presents normal local semantics, the WAL shared-memory requirement may be satisfied. Two containers on different nodes that mount the same persistent volume are a different case. The path looks identical inside each container, but the processes are not on the same host.
Kubernetes makes the visual distinction even less obvious. A PersistentVolumeClaim does not tell you whether the storage is a local block device, a network filesystem, or a distributed volume with its own consistency model. A volume that supports ReadWriteMany is specifically designed to be mounted by multiple nodes. That does not mean it is suitable for SQLite WAL.
Ask these questions before enabling WAL:
On which host does the database file physically live?
Can a process on another host open the same path?
What does the storage provider guarantee for advisory locks?
What does it guarantee for
fsync()and failover ordering?Are
-waland-shmlocal to one host or shared across hosts?Can a pod restart on another node while an old process still has the file open?
If the answer to the second question is yes, treat WAL as unsupported unless the complete architecture explicitly provides a SQLite-compatible solution. Do not infer safety from the fact that the volume is marketed as POSIX-like.
A block device attached to only one node is a different architecture from a shared filesystem mounted by several nodes. A single-node SQLite owner can use WAL on local storage even if other services reach that owner over the network.
The -wal and -shm files need operational handling
Even in a correct single-host deployment, WAL creates operational details that are easy to miss.
The -wal file can contain committed transactions that have not yet been checkpointed into the main database. The -shm file supports the wal-index. Both are part of the live database state while connections are active.
Do not copy only app.db while the database is in WAL mode and assume the copy contains every committed transaction. Use SQLite’s backup API or a tool that understands WAL and active connections. A filesystem snapshot can be safe if the storage system provides a crash-consistent snapshot across the related files, but that guarantee must be established for the specific platform.
Do not delete app.db-wal or app.db-shm as a cleanup action while a database is open. A restart may remove or rebuild some temporary state under the right conditions, but manual deletion is not a backup strategy and can destroy committed changes that have not been checkpointed.
SQLite’s WAL documentation also notes that read-only WAL databases have version-dependent requirements involving the -wal and -shm files or the directory permissions. [1] That matters for immutable images, backups, and read-only replicas. A file that opens as read-only in one SQLite version or packaging setup may fail if its companion files are absent or inaccessible.
The safest backup workflow is to use a SQLite-aware mechanism from the same host as the database process, then verify the resulting copy with an integrity check and a test restore.
How to diagnose a suspicious deployment
Start by inventorying processes, not just files.
Find every application, worker, CLI command, migration job, backup process, and sidecar that can open the database. Record the host, container, user, SQLite version, journal mode, and filesystem path for each one.
Then inspect the live journal mode from a real application connection:
PRAGMA journal_mode;
PRAGMA synchronous;
PRAGMA locking_mode;
PRAGMA busy_timeout;
journal_mode tells you whether the database is actually using WAL. synchronous tells you how aggressively SQLite syncs commits and checkpoints. locking_mode and busy_timeout help explain concurrency symptoms, but neither changes the fundamental network filesystem requirements.
Check the directory for the companion files:
ls -l --time-style=full-iso app.db app.db-wal app.db-shm
stat app.db app.db-wal app.db-shm
Look for these symptoms:
app.db-walgrows without successful checkpointsdifferent hosts report different journal modes
SQLITE_BUSYrates increase only when a second host is activereads appear stale after a remote writer commits
a restart leaves a database that needs recovery
the database works until a node fails over
the problem disappears when all traffic is pinned to one host
A particularly revealing experiment is to route every connection to one host while leaving the storage unchanged. If the issue vanishes, the filesystem may be usable for a single-host SQLite owner but not for multi-host direct access. That experiment does not prove durability, but it helps isolate the shared-host boundary.
You can also log the SQLite error code and extended error code rather than only the error string. SQLITE_BUSY, SQLITE_LOCKED, and I/O errors point to different classes of failure. Capture the operation, transaction type, host, process, and duration. A generic retry loop can hide a broken lock protocol until the database is under more load.
Do not use PRAGMA integrity_check as a continuous safety certificate. It is useful after a controlled test or recovery event, but it does not prove that the next cross-host WAL interleaving will be correct.
A failure test that is worth running
If you inherit a system that already uses a shared database path, build a disposable copy and test the architecture directly.
Run two writer processes on different hosts. Keep several readers open in explicit read transactions. Generate small commits continuously. Trigger checkpoints. Kill writers during commits. Interrupt network connectivity. Restart clients. After each round, compare expected transaction counts with the database contents and run an integrity check from a single-host SQLite process.
The point is not to certify the filesystem. The point is to discover whether the deployment already violates its assumptions under conditions that resemble failure.
Test recovery separately from ordinary operation. A database can return correct results during normal traffic and fail only when the client or server disappears after acknowledging a write. Durable database behavior is defined by what happens across interruption, not only what happens on the happy path.
If you cannot make the test environment match production’s storage implementation, do not generalize the result. A local ext4 test says little about an SMB mount. An NFS test says little about a distributed filesystem with client-side caching. A managed volume’s documentation and failure semantics matter.
Practical decision guide
Use SQLite WAL when the database file and all SQLite processes are on one host and the local filesystem provides normal locking and sync behavior. This is the common embedded-database case where WAL is useful.
Use a request API when remote clients need database access but one host can own the SQLite file. Keep the engine beside the file and make the network protocol explicit.
Use a SQLite-aware replication layer when you need copies on other hosts and the system’s consistency model fits primary/replica operation. Do not expose the same WAL files to independent SQLite processes on different nodes.
Use rollback mode only when you have a deliberate reason, a narrow access pattern, and a storage implementation you have evaluated. Treat it as a different concurrency and recovery design, not as a universal network filesystem workaround.
Use PostgreSQL or another client/server database when multiple hosts need ordinary concurrent access, independent failover, stronger operational tooling, or higher write concurrency. The database server exists partly to put the engine close to the data and to make the network protocol a first-class part of the design.
The deeper lesson
SQLite is often described as a file format with a query engine. Operationally, it is more than that. It is a coordinated set of transactions, locks, journal records, sync operations, caches, and recovery rules implemented through a VFS.
A filesystem path is only the surface of that system.
WAL makes this visible because its performance depends on a shared wal-index. The -shm file is a reminder that SQLite expects participating processes to share coordination state in a way that a local operating system can provide. A network filesystem can make a remote file look local enough for basic reads and writes. It does not automatically provide the database protocol that SQLite would need across hosts.
That is why the right architectural fix is usually not a more aggressive mount option. It is moving the boundary.
Keep SQLite and its file on the same host. Put an API, a replication protocol, or a real database server between that owner and remote clients. Once the network carries logical database operations instead of pretending to be a local disk, the system’s guarantees become much easier to state, test, and operate.
Top comments (1)
The framing that matters is the third contract , shared-memory visibility , because it's the one that fails silently. Locking violations usually error. Ordering violations usually corrupt loudly. A stale wal-index just makes a reader confidently wrong.
Two additions to the "keep the engine next to the file" rule.
The unit of locality is the process, not the host. PRAGMA locking_mode=EXCLUSIVE makes WAL viable for a single connection because it stops relying on the shared wal-index for coordination , a different failure shape rather than a free pass, since durability over the mount is still on you.
And "expose a request API instead of the file" already has tooling. Litestream replicates continuously to object storage; rqlite and LiteFS give you a network-facing SQLite. That's your rule implemented rather than described.
The line I'd put in a runbook: a mount that looks local to two hosts is a distributed system, and you adopted one without choosing to.