January 5, 2026·4 min read·engineeringrqlitesnapshotsstorage

rqlite 9.3.10 cleans up snapshots that never persist

rqlite 9.3.10 removes temporary WAL files when snapshot persistence fails or never starts. The fix belongs to the interval between creating a snapshot and committing its durable representation.

A snapshot can allocate resources before anything persists it. rqlite 9.3.10, released on 5 January 2026, closes a cleanup gap in that interval by removing temporary WAL files when persistence fails or is never invoked. The release is small, but its central change concerns an important distinction in storage code. Creating a representation of state does not mean the durable snapshot store has accepted it.

My experience with rqlite is much shorter than with SQLite. After a year with it, I value the proportionality of placing SQLite behind Raft without requiring a large operational system around it. That preference makes failure-path housekeeping relevant. A compact deployment still needs explicit ownership of the files created while it prepares recoverable state.

Snapshot creation and persistence are separate operations

The tagged state-machine wrapper exposes Snapshot, Persist, and Release as separate stages. Its snapshot wrapper records whether persistence was invoked and whether it succeeded. Release then passes those outcomes to a callback. This creates a place to handle resources whose lifetime extends beyond the function that initially prepared the snapshot.

That separation has a practical consequence for cleanup. A deferred close in the creation function addresses the descriptor's immediate lifetime. It does not necessarily remove the pathname, and it does not express what should happen if the returned snapshot object is discarded later. The owner of the final release decision needs access to the resource it must remove.

The 9.3.10 changelog identifies PR #2427 as the correction for temporary WAL files in unsuccessful or uninvoked persistence. The wording includes the second case deliberately. A cleanup path attached only to a failed write cannot run when that write was never attempted.

This is an ordinary ownership problem made less obvious by an asynchronous lifecycle. The caller receives an object representing work prepared earlier. Until the caller either persists or releases it, some resources remain associated with that object. Treating return from the preparation function as completion assigns the cleanup decision to the wrong point in time.

Incremental snapshots pass a file to the snapshot store

The store implementation at this tag explains why a temporary WAL exists. The incremental path compacts WAL content into a temporary file instead of processing a potentially large log entirely in memory. It then passes the file's path through a reader so that the snapshot system can move the file into its own storage.

The path is therefore part of a handoff protocol. Before the handoff completes, the temporary file belongs to preparation. After successful processing, its content belongs to the snapshot store. Failure or abandonment leaves a resource whose ownership must return to a cleanup path.

The patch makes the temporary file variable visible to the release callback. That callback closes the descriptor and removes the temporary pathname when one exists. The change is narrow because it uses the lifecycle already present in the wrapper. It does not require the persistence operation to manufacture a failure simply to trigger cleanup.

The distinction between closing and unlinking is important here. Closing releases the process's open handle. Removing the name releases the temporary file from the directory's set of reachable files. A correct resource lifetime can require both operations, even when normal successful persistence would already have moved the content elsewhere.

Abandonment also changes the next snapshot decision

The store treats an unsuccessful persistence outcome as a reason to require a full snapshot next time. That policy addresses the possibility that checkpointing has already changed the live WAL while the snapshot store did not finish accepting the incremental content. The new temporary-file removal complements that policy without replacing it.

These are different obligations. Removing abandoned scratch data prevents it from remaining as an orphan. Selecting a full snapshot determines how the next attempt reconstructs the state it needs. A cleanup fix should not be read as permission to discard authoritative database files or to infer snapshot validity from directory tidiness.

SQLite's checkpoint documentation explains the underlying TRUNCATE operation. It performs the waiting and checkpoint work associated with RESTART, then truncates the WAL to zero on successful completion. That is why the point at which rqlite captures incremental content matters. Checkpointing changes which file contains the state needed for subsequent work.

The failure policy also explains why a retry is not merely the same operation repeated against an unchanged input. Once part of preparation has completed, the next attempt may need a different representation. Explicitly recording that a full snapshot is required makes the retry policy part of the storage implementation.

Resource ownership remains visible through Release

The underlying snapshot object closes its source reader during Release even if persistence never happened. The store wrapper adds cleanup for the temporary WAL resource represented indirectly by that reader. Closing a reader that contains a path string is insufficient to remove the file named by the string.

That detail is easy to miss when reviewing generic interfaces. Both forms implement a reader, but one carries data and another carries a reference to data. Their resource obligations differ. A release operation that is correct for a byte stream can leave a separately named file behind if nobody owns that additional obligation.

The patch fits the quality I want from rqlite's operational model. It keeps the failure policy inside the component that understands the snapshot lifecycle. Administrators should not have to infer whether an old scratch file represents an abandoned attempt or a valid recovery artifact before every cleanup.

The resulting behavior is specific. A prepared incremental snapshot can reach Release after a failed persistence attempt or without any attempt, and its temporary WAL pathname can still be removed. The next snapshot policy remains responsible for obtaining a complete durable state after an unsuccessful handoff.