Read/Write Quorums
distributed systems
Published
January 26, 2026
In a replicated system with N nodes, each write reaches W nodes and each read contacts R nodes. The quorum intersection rule guarantees consistency when:
\[W + R > N\]
This ensures every read set overlaps with every write set by at least one node, so the read always sees the latest written value.
3-Node Cluster Demo
Select write and read quorum sizes below. The animation cycles through every possible combination of which nodes receive the write and which nodes are contacted for the read.
Code
Code
scenarios = {
const nodes = [0, 1, 2];
const writeSets = combos(nodes, W);
const readSets = combos(nodes, R);
const all = [];
for (const ws of writeSets) {
for (const rs of readSets) {
const overlap = ws.filter(n => rs.includes(n));
all.push({writeNodes: ws, readNodes: rs, overlap, success: overlap.length > 0});
}
}
return all;
}Code
html`<div style="text-align:center; margin: 8px 0 12px; padding: 10px; background: ${W + R > 3 ? '#d4edda' : '#f8d7da'}; border-radius: 6px; font-size: 15px;">
<strong>W + R = ${W + R}</strong> ${W + R > 3 ? ">" : "≤"} N = 3 →
<span style="color: ${W + R > 3 ? '#155724' : '#721c24'}; font-weight: bold;">
${W + R > 3 ? "Quorum guaranteed — every read sees the latest write" : "Quorum NOT guaranteed — some reads can miss the latest write"}
</span>
<br/>
<span style="font-size: 13px; color: #555;">
${scenarios.filter(s => s.success).length} of ${scenarios.length} scenarios return latest value,
${scenarios.filter(s => !s.success).length} return stale data
</span>
</div>`Code
// Timer: advances step when playing
{
const total = scenarios.length * 4;
const delays = [600, 1000, 1000, 1800];
let timer;
function tick() {
if (mutable playing) {
mutable step = (mutable step + 1) % total;
}
const phase = mutable step % 4;
timer = setTimeout(tick, delays[phase]);
}
timer = setTimeout(tick, delays[0]);
invalidation.then(() => clearTimeout(timer));
}Code
Code
{
const total = scenarios.length * 4;
const bs = "padding:6px 14px; border:1px solid #ccc; border-radius:5px; background:#f8f8f8; color:#000; cursor:pointer; font-size:13px;";
const div = document.createElement("div");
div.style.cssText = "display:flex; gap:8px; justify-content:center; margin:8px 0;";
const playBtn = document.createElement("button");
playBtn.style.cssText = bs;
playBtn.textContent = playing ? "Pause" : "Play";
playBtn.addEventListener("click", () => { mutable playing = !mutable playing; });
const prevBtn = document.createElement("button");
prevBtn.style.cssText = bs;
prevBtn.textContent = "Prev";
prevBtn.addEventListener("click", () => { mutable playing = false; mutable step = (mutable step - 1 + total) % total; });
const nextBtn = document.createElement("button");
nextBtn.style.cssText = bs;
nextBtn.textContent = "Next";
nextBtn.addEventListener("click", () => { mutable playing = false; mutable step = (mutable step + 1) % total; });
div.appendChild(playBtn);
div.appendChild(prevBtn);
div.appendChild(nextBtn);
return div;
}Code
{
const width = 500, height = 310;
const nodeR = 32;
const nodeData = [
{x: 100, y: 190, label: "A"},
{x: 250, y: 190, label: "B"},
{x: 400, y: 190, label: "C"}
];
const clientX = 250, clientY = 40;
const {scenario, phase, scenarioIndex, totalScenarios} = animState;
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("width", "100%")
.style("max-width", width + "px")
.style("display", "block")
.style("margin", "0 auto")
.style("background", "#fafafa")
.style("border", "1px solid #e0e0e0")
.style("border-radius", "8px");
// Arrowhead markers
const defs = svg.append("defs");
for (const [id, color] of [["arr-w", "#4a90d9"], ["arr-r", "#e67e22"]]) {
defs.append("marker").attr("id", id)
.attr("viewBox", "0 0 10 10").attr("refX", 9).attr("refY", 5)
.attr("markerWidth", 6).attr("markerHeight", 6).attr("orient", "auto")
.append("path").attr("d", "M0,0 L10,5 L0,10 z").attr("fill", color);
}
// Client box
svg.append("rect")
.attr("x", clientX - 40).attr("y", clientY - 16)
.attr("width", 80).attr("height", 32).attr("rx", 5)
.attr("fill", "#f0f0f0").attr("stroke", "#888").attr("stroke-width", 1.5);
svg.append("text")
.attr("x", clientX).attr("y", clientY + 5)
.attr("text-anchor", "middle").attr("font-size", 13).attr("fill", "#333")
.text("Client");
// Cluster lines between nodes
for (let i = 0; i < 3; i++) {
for (let j = i + 1; j < 3; j++) {
svg.append("line")
.attr("x1", nodeData[i].x).attr("y1", nodeData[i].y)
.attr("x2", nodeData[j].x).attr("y2", nodeData[j].y)
.attr("stroke", "#e0e0e0").attr("stroke-width", 1.5)
.attr("stroke-dasharray", "4,4");
}
}
// Shorten a line segment by s1 at start and s2 at end
function shorten(x1, y1, x2, y2, s1, s2) {
const dx = x2 - x1, dy = y2 - y1;
const len = Math.sqrt(dx * dx + dy * dy);
if (len === 0) return {x1, y1, x2, y2};
return {
x1: x1 + dx * s1 / len, y1: y1 + dy * s1 / len,
x2: x2 - dx * s2 / len, y2: y2 - dy * s2 / len
};
}
// Write arrows (phase 1+)
if (phase >= 1) {
for (const ni of scenario.writeNodes) {
const n = nodeData[ni];
const l = shorten(clientX - 6, clientY, n.x - 6, n.y, 22, nodeR + 4);
svg.append("line")
.attr("x1", l.x1).attr("y1", l.y1).attr("x2", l.x2).attr("y2", l.y2)
.attr("stroke", "#4a90d9").attr("stroke-width", 2)
.attr("marker-end", "url(#arr-w)");
}
}
// Read arrows (phase 2+)
if (phase >= 2) {
for (const ni of scenario.readNodes) {
const n = nodeData[ni];
const l = shorten(n.x + 6, n.y, clientX + 6, clientY, nodeR + 4, 22);
svg.append("line")
.attr("x1", l.x1).attr("y1", l.y1).attr("x2", l.x2).attr("y2", l.y2)
.attr("stroke", "#e67e22").attr("stroke-width", 2)
.attr("stroke-dasharray", "6,3")
.attr("marker-end", "url(#arr-r)");
}
}
// Draw nodes
for (let i = 0; i < 3; i++) {
const n = nodeData[i];
const isWritten = scenario.writeNodes.includes(i);
const isRead = phase >= 2 && scenario.readNodes.includes(i);
const isOverlap = phase >= 2 && scenario.overlap.includes(i);
let fill = "#e8e8e8", stroke = "#aaa", sw = 2;
if (phase >= 1 && isWritten) {
fill = "#4a90d9";
stroke = "#357abd";
}
if (isOverlap) {
fill = "#27ae60";
stroke = "#1e8449";
sw = 3;
} else if (isRead && !isWritten) {
stroke = "#e67e22";
sw = 3;
}
svg.append("circle")
.attr("cx", n.x).attr("cy", n.y).attr("r", nodeR)
.attr("fill", fill).attr("stroke", stroke).attr("stroke-width", sw);
const textColor = (phase >= 1 && isWritten) || isOverlap ? "#fff" : "#333";
svg.append("text")
.attr("x", n.x).attr("y", n.y - 6)
.attr("text-anchor", "middle").attr("font-size", 14).attr("font-weight", "bold")
.attr("fill", textColor)
.text(n.label);
svg.append("text")
.attr("x", n.x).attr("y", n.y + 14)
.attr("text-anchor", "middle").attr("font-size", 12)
.attr("fill", textColor)
.text(phase >= 1 && isWritten ? "v2" : "v1");
}
// Phase description
const phaseTexts = [
"All nodes start with v1",
"Write v2 → " + scenario.writeNodes.map(i => nodeData[i].label).join(", "),
"Read ← " + scenario.readNodes.map(i => nodeData[i].label).join(", "),
scenario.success
? "✓ Read returns v2 (overlap: " + scenario.overlap.map(i => nodeData[i].label).join(", ") + ")"
: "✗ Read returns stale v1 — no overlap!"
];
svg.append("text")
.attr("x", width / 2).attr("y", height - 30)
.attr("text-anchor", "middle").attr("font-size", 14)
.attr("fill", phase === 3 ? (scenario.success ? "#1e8449" : "#c0392b") : "#333")
.attr("font-weight", phase === 3 ? "bold" : "normal")
.text(phaseTexts[phase]);
svg.append("text")
.attr("x", width / 2).attr("y", height - 10)
.attr("text-anchor", "middle").attr("font-size", 11).attr("fill", "#999")
.text("Scenario " + (scenarioIndex + 1) + " of " + totalScenarios);
return svg.node();
}Code
html`<div style="display:flex; gap:20px; justify-content:center; font-size:12px; color:#555; margin-top:6px;">
<span><span style="display:inline-block;width:14px;height:14px;background:#4a90d9;border-radius:50%;vertical-align:middle;margin-right:4px;"></span> Written (has v2)</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#e8e8e8;border:2px solid #e67e22;border-radius:50%;vertical-align:middle;margin-right:4px;"></span> Read (has v1)</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#27ae60;border-radius:50%;vertical-align:middle;margin-right:4px;"></span> Overlap (read finds v2)</span>
<span><span style="display:inline-block;width:14px;height:14px;background:#e8e8e8;border:2px solid #aaa;border-radius:50%;vertical-align:middle;margin-right:4px;"></span> Not contacted</span>
</div>`