rqlite separates read replicas from voting members
rqlite 5.1.0 adds non-voting replicas and a freshness control for local reads. Read capacity can grow without increasing the voting membership, provided callers choose the intended consistency level.
Adding database replicas can increase read capacity while also making consensus more expensive. rqlite 5.1.0 separates those two decisions by introducing read-only, non-voting nodes. The January 10 announcement explains that these replicas receive the database's changes without participating in Raft consensus. Previously, every added node participated, so expanding the cluster could affect write throughput.
The new node type has a precise role. It keeps a local SQLite database updated from committed log entries, but it does not contribute to quorum or vote in leader elections. Its usefulness depends on clients reading that local copy deliberately. A request that still requires the leader does not become a distributed local read merely because it first reaches a replica.
I value rqlite's proportional approach to distributed relational state. SQLite supplies the local database, and Raft supplies the agreement needed to replicate changes. Non-voting replicas make that division more flexible by allowing extra read locations without treating every one as another participant in agreement.
Replication and voting are separate responsibilities
The read-only-node documentation in the release tag says these nodes subscribe to the leader's stream of committed log entries. They apply those entries to their SQLite databases like other nodes. The omitted responsibility is voting, including participation in the quorum required for agreement.
The cluster guidance for this version recommends practical voting memberships of three, five, seven or nine. Its explanation focuses on the majority needed before a change can proceed. More voting nodes increase the number that must agree, and even-sized memberships do not necessarily add failure tolerance compared with the preceding odd size.
For example, the documented comparison uses three voting nodes and four voting nodes. Both can tolerate one failed voter, because the required majorities are two and three respectively. A fourth non-voting replica has a different purpose. It provides another copy for reads without making the voting membership four.
That distinction prevents a misleading node-count calculation. A cluster with several read replicas does not gain the voting failure tolerance associated with the same number of voters. The replicas continue to depend on the voting cluster for committed changes. They are additional consumers of replicated state, with a role chosen for read access.
The release documentation presents the feature as a way to improve read scalability without adding write latency from extra consensus participation. That describes the protocol role being avoided. It should not be expanded into a claim that replicas consume no network or storage resources. They still receive committed entries and update local databases, which is the work that makes their reads possible.
The startup flag chooses the role
The new role is enabled with -raft-non-voter=true. Otherwise the documentation says a read-only node joins and leaves through the same cluster-management operations as a voter. A replica therefore remains a managed cluster member even though its presence does not increase voting membership.
An illustrative launch on a separate host can combine the documented cluster flags with the new role flag. It assumes the hostnames resolve and the relevant addresses are reachable.
rqlited -node-id reader-1 \
-http-addr reader1:4001 \
-raft-addr reader1:4002 \
-raft-non-voter=true \
-join http://host1:4001 \
~/reader-1
The HTTP address is where clients reach the API, while the Raft address serves cluster communication. A unique node ID identifies the member within the cluster. The data-directory argument supplies a location for this node's state. None of those meanings disappear when the member is a non-voter.
The cluster documentation also allows joining through a node that is not currently leader, because join requests can redirect to the leader. The membership operation still needs the leader's participation. Starting a process with the read-only flag does not create an independent database that happens to resemble the cluster. Joining establishes how it receives the replicated state it will serve.
Membership identity also matters when an address changes. The cluster documentation says a node whose HTTP or Raft address changes between restarts should explicitly join again with the new address. The leader updates its membership record through that operation. A read-serving process's reachable address and its identity in the cluster therefore need to agree, even when the process does not vote.
For an ordinary restart, the documentation says the existing member can retain its original join parameters. A repeated join for the same member is treated as a restart rather than a new independent member. This helps keep launch instructions stable, while uniqueness of node IDs still distinguishes replicas from each other. Reusing one ID for several simultaneous replica processes would contradict that requirement.
The read-only documentation makes removal part of the same management model as other members. Retiring a replica is consequently a membership operation as well as stopping a process, because the cluster otherwise retains the record to which it sends updates.
Local reads require an explicit consistency choice
The read-consistency documentation for 5.1.0 defines none, weak and strong levels. With none, a node reads its local SQLite database without checking that it is the leader. This is the level required for serving a read from a read-only replica. Other levels send the request toward the leader instead.
The default is weak. It checks local leader state before querying the local database. The documentation identifies a small interval in which leadership can change around that check, allowing stale results. strong sends the read through the Raft system and requires communication with at least a quorum. That adds agreement work to the read path.
These levels express different contracts per request. A replica does not silently reinterpret a leader-oriented request as permission to return its local state. A client that wants replica read capacity needs to select none. This keeps the location and freshness decision visible at the call site instead of deriving it only from the host chosen by a load balancer.
An illustrative request uses the release's documented endpoint and query-string syntax. It assumes a table called settings already exists.
curl -G 'http://reader1:4001/db/query?level=none&freshness=1s' \
--data-urlencode 'q=SELECT * FROM settings'
The selected host is the read replica. The explicit consistency level allows it to serve from local SQLite, and the freshness argument adds a separate admission check. Omitting the level would select the default leader-oriented behavior, which would defeat the purpose of asking this node to serve a local read.
Freshness checks contact age
Version 5.1.0 also introduces a freshness control for none reads. The documented value is a duration string. The node compares that duration with the time since its last contact with the leader and returns an error when the contact is too old. A value of zero disables the freshness check.
This matters when a replica becomes disconnected. Without an admission check, a reachable node can keep returning its local state even after it stops receiving cluster updates. The request can now decline that state once the leader-contact interval exceeds its chosen limit. That is a useful failure behavior for a client that can tolerate some staleness but does not want unlimited disconnection.
The store implementation at this tag makes the measurement concrete through LastContact() and elapsed time. The check does not compare an application timestamp on every returned row with the current time. Nor does it demonstrate that the replica has applied every write a particular client has just completed. Its direct measurement is contact age.
That distinction follows from the documented reasons a local read may be stale. A node can fall behind while still belonging to the cluster, or it can stop receiving updates after losing contact. A contact-age threshold addresses the latter concern directly, while a client requiring quorum-backed processing has a different available consistency level. The freshness parameter should be explained using the evidence it actually inspects.
I prefer making that tradeoff explicit per request. Some reads can use a recently connected local copy, while other operations need the stronger path. Non-voting replicas expand where the first kind can run without changing the voting membership used by the second. The client still chooses the consistency behavior needed for the meaning of its particular read.
The resulting topology has a voting cluster responsible for agreement and additional nodes responsible for serving selected local reads. Each replica receives the committed statement stream and maintains SQLite state, but does not enlarge the electorate. That lets the read-serving membership grow independently of the majority calculation, while the request's level and freshness threshold determine when its local copy is acceptable.