Why architecture matters here

Speculative retry is architectural because tail latency in a replicated store is an emergent property of the whole fleet, not of any one request. With hundreds of nodes, at any given moment some small fraction are doing something that makes them briefly slow: a stop-the-world GC pause, a major compaction pinning the disk, a kernel flushing dirty pages, a noisy neighbor on shared hardware. The probability that a specific node is slow right now is low, but the probability that at least one of the replicas your read must wait for is slow is much higher, and it grows with the consistency level and the cluster size. Fixed-replica reads inherit the maximum latency of the replicas they touch, so their tail is set by the unluckiest node in each request, not the typical one.

The insight speculative retry exploits is statistical independence. Two different replicas are, to a good approximation, having their bad moments independently — one node's GC pause tells you nothing about another node's disk. So if the coordinator can detect that a read is taking unusually long and route a copy to a second replica, the odds that both the original and the hedge are slow at the same instant are the product of two already-small probabilities, which is tiny. That is why a single hedge collapses the tail so effectively: it converts 'wait for a possibly-slow replica' into 'wait for the faster of two independent replicas', and the faster-of-two distribution has a far shorter tail than either one alone.

But the same independence that makes hedging effective makes over-hedging dangerous. Each hedge is real work — a full read on an extra node — and if you hedge a large fraction of reads you have simply increased the cluster's read load by that fraction. Under normal conditions that is a modest tax. Under stress, when many nodes are slow at once because the whole cluster is overloaded, the threshold trips constantly, hedges fire everywhere, and the extra load makes the overload worse: a positive feedback loop where slowness causes hedges and hedges cause more slowness. The architecture must therefore bound amplification, which is exactly why 'ALWAYS' is a footgun and why an adaptive percentile threshold — one that only trips when a replica is slow relative to the table's own recent normal — is the sane default.

There is a deeper reason the threshold has to be adaptive rather than a fixed number, and it explains the whole PERCENTILE design. A table's normal latency is not a universal constant; it depends on the data model, the partition sizes, the read pattern, and the hardware, and it drifts over time as data grows and workloads shift. A fixed 50ms threshold that is generous for a wide-partition analytical table is far too slow for a point-lookup table whose normal read is under a millisecond — on the fast table a genuinely stuck replica would blow past its real p99 twenty times over before a 50ms fixed threshold even noticed. Conversely, a fixed threshold tuned for the fast table would trip on every ordinary read of the slow one, hedging constantly for no benefit. Only a threshold expressed as a percentile of the table's own live latency distribution stays correct as conditions change: it fires when this table's read is slow for this table, which is the only definition of 'slow' that means anything. That self-calibration is what lets one mechanism serve tables with wildly different performance profiles without per-table hand-tuning of millisecond constants, and it is why the feature ships tracking latency per table rather than globally.

Advertisement

The architecture: every piece explained

Start with the coordinator. Any node can coordinate a read: it receives the client request, determines which replicas own the partition, and sends the read to the number of replicas the consistency level requires — one for the fastest, a majority for QUORUM, and so on. Normally it waits for those replicas to answer. Speculative retry inserts a timer into that wait. Cassandra sends a full data read to one replica and lightweight digest reads (a hash of the result rather than the data) to the others, so it can confirm the replicas agree without shipping the payload multiple times.

The latency tracker is what makes the threshold adaptive. For each table Cassandra continuously samples read latencies and maintains a running estimate of the distribution, so it can answer 'what is the p99 read time for this table right now'. The threshold configured on the table — the speculative_retry option — selects the policy: PERCENTILE (e.g. 99PERCENTILE, hedge if a replica has not answered by the current p99), a fixed millisecond value (50ms, hedge after a hard deadline regardless of the distribution), ALWAYS (send the extra read immediately with every request), or NONE (never hedge; accept the tail). PERCENTILE is the usual choice because it self-calibrates to the table.

Now the hedge itself. When the timer trips because a replica is a slow replica — stuck in a GC pause or behind a compaction — the coordinator picks an extra replica that owns the same data but was not part of the original read set, and fires the read to it. Both the original and the hedge are now in flight. The first to answer with a valid response wins; the coordinator uses it and ignores the straggler when it eventually replies. Consistency is preserved because the hedge is a real replica of the same partition and its response is subject to the same digest match checks — if digests disagree, the normal read-repair path reconciles them, exactly as it would without hedging.

The costs and controls sit at the bottom. ALWAYS / NONE are the extremes: ALWAYS doubles read work to minimize tail, NONE does zero extra work and lives with the tail. The extra load cost is the crux — every hedge is redundant read work, so the aggregate cost is proportional to how often the threshold trips, which is why a percentile threshold that trips only on genuine outliers is far cheaper than a fixed threshold set too tight. The ops strip is the operating discipline: prefer PERCENTILE, monitor the hedge (speculative-retry) rate as a first-class metric, cap the amplification so a cluster-wide slowdown cannot trigger a hedge storm, and tune the policy per hot table rather than globally.

Speculative retry — beat the tail by asking a spare replica before the slow one answersmeasure latency, fire a hedge, take the first good replyCoordinatorreceives readPrimary replicassent for CLLatency trackerper-table p99 sampleThresholdPERCENTILE / fixed msSlow replicaGC pause / hot diskExtra replicahedge request firedFirst to answerwins the readDigest matchconsistency preservedALWAYS / NONEhedge every / neverExtra load costredundant read workOps — pick PERCENTILE, watch hedge rate, cap amplification, tune per hot tableone slowhedgesampletripcostraceverifyoperateoperate
Speculative retry: the coordinator sends a read to the replicas required for the consistency level, tracks per-table latency, and if a replica has not answered by the threshold it fires an extra read to a spare replica and takes whichever good reply lands first.
Advertisement

End-to-end flow

Trace a QUORUM read on a three-replica keyspace. A client asks the coordinator for a row; the replication factor is 3 and QUORUM needs 2 of them. The coordinator sends a full data read to replica A and a digest read to replica B (two responses satisfy QUORUM), holding replica C in reserve. In the happy case A and B both answer in three milliseconds, their digests match, and the coordinator returns the row. No hedge fired; speculative retry cost nothing. This is the overwhelming majority of reads, and it is important that the mechanism is invisible and free when the cluster is healthy.

Now the interesting case: replica A enters a stop-the-world GC pause just as the read lands, and stops responding. The coordinator's timer is set to the table's current p99 — say four milliseconds. At four milliseconds A still has not answered. The coordinator does not keep waiting; it fires the same read at reserve replica C. C is healthy and answers in another two milliseconds. The coordinator now has responses from B (the original digest) and C (the hedge) — two replicas, QUORUM satisfied, digests checked — and returns the row about six milliseconds after the request. Without the hedge it would have waited out A's GC pause, which might be eighty milliseconds. The tail was cut by more than a factor of ten, at the cost of one extra read.

Consider what the digest machinery bought here. When C's data answer came back, its digest was compared against B's; they matched, so the row is known-consistent and no repair was needed. Suppose instead C had a stale copy — its digest disagrees. The coordinator then does a full data read from the disagreeing replicas, reconciles to the newest version by timestamp, returns the correct row, and schedules a read repair to fix the stale replica in the background. The hedge did not weaken consistency at all: it changed which replicas answered, but every response still passed the same reconciliation the consistency level demands. Speculative retry is a latency optimization layered strictly on top of the existing consistency protocol, never a bypass of it.

Now the failure the operator must design against: a cluster-wide slowdown. At peak traffic every node's read latency creeps up because disks are busy with compaction. If the table were configured with a tight fixed threshold — say 10ms — then when every replica is legitimately taking twelve milliseconds, the threshold trips on nearly every read and the coordinator hedges almost everything. Read load jumps by close to fifty percent exactly when the cluster is already saturated, the extra reads make the disks busier, latency climbs further, more reads trip the threshold, and the cluster spirals into a hedge storm. A PERCENTILE threshold sidesteps this: because it tracks the table's own live p99, when the whole table slows down uniformly the p99 rises with it, so ordinary-for-now reads do not look like outliers and the hedge rate stays bounded. The mechanism hedges only the replicas that are slow relative to current conditions, which is precisely the behavior that keeps it from amplifying an overload into an outage.