March 10, 2026·4 min read·engineeringrqlitewalconcurrency

rqlite 9.4.5 gives checkpoint waiting back to SQLite

rqlite 9.4.5 removes its outer polling loop around checkpoint truncation. SQLite's busy handler now supplies the wait, with the caller still responsible for excluding committing writers.

rqlite's checkpoint-truncation code had a retry loop around an operation that already knew how to wait. Version 9.4.5, released on 10 March 2026, removes that outer loop and relies on SQLite's busy handling. The change follows several corrections to checkpoint completion behavior. Its significance is the ownership of waiting, because two layers managing the same contention can obscure what a timeout actually means.

My interest in rqlite comes from the amount of distributed systems machinery it supplies for a relational workload. After a year with it, I value changes that keep coordination in the layer that understands it. SQLite already knows which readers prevent WAL reuse. rqlite still owns the surrounding snapshot policy, including when it is safe to request truncation.

The earlier path repeatedly invoked the checkpoint

The release commit shows the old implementation. It attempted a checkpoint, inspected completion, and used a ticker to retry until a separate timeout fired. The replacement sets the connection's busy timeout to the supplied duration and invokes the checkpoint once. A nonzero completion status becomes an error.

Those approaches place the wait at different levels. An outer retry observes a completed attempt and begins another. An engine-level busy handler participates in the attempt while SQLite manages its lock requirements. Removing the outer loop reduces the number of independent waiting policies involved in that operation.

The simplification does not make contention disappear. It changes how the implementation waits for the conditions required by the checkpoint. A reader that retains a relevant lock can still prevent success within the configured interval. The operation must report that outcome instead of pretending that partially completed work met the caller's contract.

Truncating a WAL requires more than copying pages

SQLite's checkpoint API distinguishes checkpoint modes. RESTART waits until readers no longer use the WAL after completing the required copy work. TRUNCATE adds reducing the WAL file to zero bytes before successful return. That extra condition matters to code whose next action assumes that the log begins afresh.

The ordinary WAL model permits readers to retain a view of older content while a writer appends newer changes. SQLite's WAL documentation explains how reader end marks determine the pages they can use. A checkpoint must respect those readers, so progress in copying pages does not automatically imply permission to reuse or truncate the log.

For snapshot code, this distinction is part of correctness. An operation named checkpoint can sound complete once its copy work finishes. A caller that requires truncation needs the stronger outcome expressed by the chosen mode and its return status. The release keeps that stronger requirement visible.

The caller still controls writers

The tagged database implementation documents a prerequisite for CheckpointTruncateWithTimeout. Its caller must guarantee that no write transactions commit for the duration of the call. If readers release their locks in time, the method can complete. If they retain them throughout the interval, it returns an error.

This divides responsibility clearly. The caller provides the interval without committing writers. SQLite manages the lock waiting needed inside that interval. The timeout bounds a wait for readers under those conditions, so it should not be interpreted as a general solution to arbitrary concurrent access.

The method also preserves the prior busy-timeout setting and restores it afterward. It temporarily uses FULL synchronous mode for the checkpoint, then restores the previous mode. These settings belong to the operation's scope, rather than silently becoming a permanent policy for unrelated database work.

A busy timeout belongs to a connection

SQLite's busy-timeout interface installs a handler on a database connection. The handler sleeps repeatedly until the configured amount of waiting has accumulated, then gives up with a busy outcome. A connection has only one busy handler, so setting a timeout replaces a previously installed handler.

That connection scope explains the restoration code. Temporarily changing the handler to support a maintenance operation can otherwise change how later requests behave. Resource management includes restoring policy as well as releasing a lock or closing a file.

The March changelog history records the removal of an earlier forced-truncation mechanism because it could deadlock. It also records the requirement that checkpoint truncation complete or the process exit in the relevant store path. The latest simplification sits within that sequence of work on making completion conditions explicit.

This is the kind of maintenance change that improves the explanation of a system as well as its code. One layer decides when the operation is allowed. The engine waits on its own lock conditions, and the caller receives an outcome it can use for snapshot policy. The next action can depend on a completed truncation without maintaining a second polling protocol around it.