# When must a Raft leader refuse to serve reads after losing lease quorum? **Date of Record:** 2026 November 14 **Scope:** Applies to Raft (and Raft-derived) clusters serving linearizable reads via leader leases or ReadIndex, where the leader may be partitioned from a quorum but still reachable by clients. Does not apply to leader-election safety itself (which remains correct); it concerns the read-path liveness/safety boundary during the window between lease expiry and step-down. ## Core Distinction The distinction is between *holding leadership* and *holding a valid lease*. A Raft leader retains the `leader` role in its local state machine until it observes a higher term or a quorum-backed step-down. During a network partition where the leader is isolated from the majority, the leader's local term does not advance, and it will continue to answer client requests. The critical invariant: **leadership is not a proof of authority to serve linearizable reads; only a quorum-confirmed lease or a successful ReadIndex round-trip is.** A leader that has lost quorum contact must treat every read as unconfirmed even though `state == Leader` and `commitIndex` is locally stable — because a new leader may already have been elected in the majority partition and may have committed writes the isolated leader cannot see. ## What Was Observed In a five-node cluster (Raft, 500 ms heartbeat, 1.5 s election timeout, leader lease of 2 s), a partition isolated the leader L with two followers. Clients pinned to L continued to receive successful linearizable reads for approximately 1.8 s after the majority partition elected a new leader L' and committed a configuration-changing write. The isolated leader's lease had not yet expired by its own clock, and its ReadIndex path returned the locally cached `commitIndex` because the heartbeat round-trip to a quorum was still *pending* rather than *failed* — the gRPC calls had not yet hit their deadline. Reads served in that window returned stale values: a key updated on L' read back its pre-update value from L. The failure was not in the election protocol; it was in the read path treating "no error yet" as "quorum confirmed." ## Condition of Validity The refusal rule is valid — and the stale-read hazard exists — only when all of the following hold: 1. Reads are served as *linearizable* (not stale/bounded-staleness reads, which are exempt by contract). 2. The leader uses a time-based lease derived from a *local* monotonic clock, not a quorum-acknowledged timestamp per read. 3. The client's session does not carry a read-your-writes token that would force routing to the current leader. 4. The partition is asymmetric or slow (gray failure), so quorum RPCs hang rather than fail fast. If reads use per-request ReadIndex with a hard deadline strictly less than the election timeout minus clock-skew margin, the hazard collapses to a latency cost, not a correctness violation. ## Decision Boundary The leader must refuse (return `NotLeader` / redirect) a linearizable read when **any** of these is true at request admission: - `now - lastQuorumAckTime > leaseDuration - maxClockSkew`. - The in-flight ReadIndex heartbeat quorum has not acknowledged within `readDeadline`, where `readDeadline < electionTimeout - maxClockSkew`. - A `CheckQuorum` probe in the current heartbeat interval failed to reach a majority. It may serve the read when the quorum-ack timestamp is fresh *and* the deadline margin holds. The non-obvious boundary: the check must be evaluated *per read at admission*, not once per lease renewal, because a lease that was valid at renewal can be invalidated by a partition that begins mid-lease. Caching "lease valid" as a boolean and trusting it until expiry reintroduces the 1.8 s stale window. ## Operational Effect Enforcing per-read lease validation converts a silent linearizability violation into a bounded, visible availability dip: clients on the isolated leader receive `NotLeader` redirects within one read-deadline (≈ 300–500 ms) instead of stale data for up to a full lease. The cost is that legitimate reads during transient heartbeat jitter are rejected and must retry against the true leader, raising p99 latency and client retry volume. The benefit is that the linearizability contract holds under partition, and the failure surfaces as an explicit, alertable error class rather than a data-integrity incident discovered downstream. Operators should size `leaseDuration` and `readDeadline` against measured clock skew and p99 heartbeat RTT, and alert on the `NotLeader` redirect rate as an early partition indicator. ```python import time from dataclasses import dataclass class StaleReadRefused(RuntimeError): """Raised when a leader cannot prove quorum-backed authority for a linearizable read.""" pass @dataclass class LeaderReadGate: lease_duration_s: float max_clock_skew_s: float last_quorum_ack_monotonic: float # time.monotonic() of last majority heartbeat ack read_deadline_s: float def _lease_margin_ok(self, now: float) -> bool: effective = self.lease_duration_s - self.max_clock_skew_s return (now - self.last_quorum_ack_monotonic) <= effective def admit_linearizable_read(self, now: float, quorum_ack_within_deadline: bool) -> None: if not self._lease_margin_ok(now): raise StaleReadRefused( f"lease margin exhausted: age={now - self.last_quorum_ack_monotonic:.3f}s " f"> effective={self.lease_duration_s - self.max_clock_skew_s:.3f}s" ) if not quorum_ack_within_deadline: raise StaleReadRefused( f"ReadIndex quorum not confirmed within {self.read_deadline_s:.3f}s; " "refusing to serve possibly-stale linearizable read" ) # Safe to serve: quorum-backed authority proven at admission time. def serve_read(gate: LeaderReadGate, quorum_ack_within_deadline: bool) -> str: now = time.monotonic() gate.admit_linearizable_read(now, quorum_ack_within_deadline) return "value" ```