October 31, 2025·6 min read·engineeringrqliterecoverysnapshotsapi

rqlite 9.2.0 reuses the local database during eligible restarts

Startup can skip restoring a snapshot when a recorded fingerprint matches the local SQLite file. Associative RETURNING responses also reach the execute endpoint.

Restoring a database that is already present can make restarting a node proportional to the amount of data it holds. rqlite 9.2.0, released on 31 October, adds a startup path that reuses the local SQLite database when a recorded fingerprint matches. The optimization removes a redundant snapshot restore in eligible cases.

The release notes describe the earlier startup cost as growing linearly with SQLite data size. They advertise substantially faster startup for large databases. The implementation gives that performance claim a concrete condition. Startup needs the evidence expected by the reuse path, and falls back to restoration when that evidence is absent.

I value rqlite's modest deployment requirements, but startup time is part of operating cost too. Rebuilding a large local database during routine restart can make a small process harder to maintain. Avoiding that work is useful when the server can recognize a local file suitable for reuse.

Startup checks a recorded file fingerprint

PR #2371 introduces the fingerprint and the startup decision. The record contains the main database file's modification time and size. Its comparison method checks equality of both fields.

The released startup implementation first requires an available snapshot before considering this path. It then looks for the clean-snapshot marker, reads the fingerprint, and obtains the current file's modification time and size. A missing marker, unreadable fingerprint or mismatch leaves the normal restoration decision in place.

When the checks succeed, startup sets NoSnapshotRestoreOnStart in the Raft configuration and keeps the existing database files. It reads the latest snapshot's index and term, then assigns the corresponding local state-machine position and database-applied index.

These are different kinds of information. The fingerprint describes properties of the local file. Snapshot metadata supplies the replication position used when startup accepts that file. This implementation coordinates those inputs without copying the snapshot's database contents into a new local database.

The record in this release is not a full content hash. The fingerprint structure contains modification time and size. Calling it a checksum would imply an additional read and comparison that the code does not perform.

Successful snapshot work creates the marker

The startup decision depends on evidence written earlier in the snapshot lifecycle. Before beginning new snapshot work, the store removes the existing clean-snapshot marker. That prevents a failed new snapshot attempt from leaving the previous marker untouched as evidence of current success.

The code temporarily selects SQLite's FULL synchronous mode during snapshot work. Its comment explains the intended invariant that a successful snapshot leaves a synchronized database file on disk. It also restores the normal setting afterwards because keeping FULL mode permanently has a write-performance cost.

The snapshot result receives a finalizer that creates the fingerprint. That helper obtains the main database file's modification time and size, serializes the record to a temporary path, and renames it to the clean-snapshot marker path.

Writing the fingerprint itself includes a file synchronization call. The helper therefore does more than leave an in-memory note that the snapshot completed. It creates a durable artifact that a later process can inspect after restart.

A representative fingerprint has these fields. The values here are illustrative file metadata, not measurements of a particular deployment.

{
  "mod_time": "2025-10-31T10:00:00Z",
  "size": 1048576
}

The timestamp and size must match the file observed at startup. Merely finding a file named clean_snapshot does not satisfy the predicate. Startup must also decode its contents and compare them with the current database file.

Restoration remains an active path

The optimization does not remove snapshot restoration from the server. If reuse checks fail, startup follows its existing recovery route. The change is a conditional shortcut, with counters distinguishing startup restores from skipped restores.

Explicit recovery through the peers-file mechanism also removes the clean-snapshot marker. The code comments state that recovering a node invalidates the existing SQLite file for this purpose. That operation needs its own recovery procedure, rather than trusting the marker as an unconditional instruction to reuse.

During restoration, the store removes the marker before replacing the database. After swapping in the restored file, it creates a new fingerprint. This connects the marker's lifecycle to the actual database replacement instead of allowing metadata for the old file to remain authoritative.

These conditions explain why startup performance should be described in terms of eligible reuse. A node with matching metadata avoids a full restoration copy. A node without that evidence still performs recovery work appropriate to its state.

My preference is to retain that distinction in operating documentation. A release can remove the normal data-size-dependent restore without promising that every possible restart becomes independent of recovery conditions. The counters and explicit decision path make it possible to identify which behavior occurred.

Restart tests examine the decision

PR #2375 expands restart coverage and adds counters used by the tests. The tests check whether startup restored a snapshot or skipped restoration, alongside verifying the reopened store. This observes the optimization's chosen path rather than judging success only by how quickly the test ends.

That distinction is useful for performance-related correctness. A healthy database can reopen successfully through either route. A test that only checks returned rows may pass even when the new optimization never activates. Counting the selected route establishes that the reuse branch was exercised.

The same change introduces a store option used to suppress snapshot-on-close in testing. Normal close still attempts a snapshot to minimize later startup work. Separating those cases lets tests examine restarts with different preparation states.

The fingerprint tests also exercise mismatches directly. After writing and reading a record, they require its original timestamp and size to compare equal. Increasing the supplied size by one must fail comparison. Advancing the supplied timestamp by one second must also fail. These assertions make both parts of the predicate necessary, instead of allowing one matching attribute to compensate for the other.

A missing fingerprint file has its own test and produces a read error. Startup treats that inability to read the marker as a reason to leave restoration enabled. It does not require a deployment to manufacture a marker before opening an older data directory.

Shutdown work and startup work are related here. The implementation can spend effort making a clean snapshot before closing, then use the resulting marker on the next open. Describing only the improved startup would omit the preparation that helps make that path available.

RETURNING gains the associative representation

A separate change in this release addresses response formatting. PR #2373 makes the execute handler honor the associative response option and the blob-array option. The relevant SQL feature already returns rows from writes. This patch makes its representation consistent with the selected output form.

For a table with integer id and text name columns, the system test issues an insert with RETURNING * while requesting associative. Its expected result uses row objects keyed by column name, alongside an object describing column types.

{
  "results": [{
    "types": {"id": "integer", "name": "text"},
    "rows": [{"id": 7, "name": "siobhan"}]
  }]
}

The corresponding positional form uses a columns array and values arrays. Associative output changes how returned data is represented, not the write operation's SQL semantics. Clients that prefer objects can now request that form through the execute endpoint.

The tests cover an update returning no matching rows as well. Its associative response has the expected type mapping and an empty rows array. An ordinary insert without RETURNING has write-result metadata and a null rows value in the tested associative form.

Those cases help distinguish a write that returned no rows from a write that did not request row results. A client serializer should preserve that distinction if its application depends on the result shape. Treating every empty-looking response as the same outcome would discard information that the server now represents.

Two changes reduce different operating costs

The startup optimization concerns work performed before the database resumes service. The associative response change concerns work a client performs after a write returns data. They share a release but operate at separate layers.

For a large local database, the new marker-based decision can avoid copying snapshot contents during an eligible restart. For a client consuming RETURNING results, the execute endpoint can supply named row objects directly. Both behaviors are inspectable in the released code, with recovery eligibility and response shape defined by their respective checks.