System Design (High-Level Design) Module 10 – Consistency, time and coordination
Consistency models, from linearisable to eventual
What linearisable, sequential, causal and eventual consistency promise, where session guarantees fit, and how to pick the weakest model each operation can use.
What you will learn
- Define linearisability, sequential, causal and eventual consistency from a history of operations
- Check whether a small history is linearisable or sequentially consistent
- Map the session guarantees onto the same hierarchy of models
- Pick the weakest model that keeps each operation of a product correct
Before you start
What you’ll learn · Before you start
What you will learn
- Define linearisability, sequential, causal and eventual consistency from a history of operations
- Check whether a small history is linearisable or sequentially consistent
- Map the session guarantees onto the same hierarchy of models
- Pick the weakest model that keeps each operation of a product correct
Before you start
On this page
A user changes their delivery address, refreshes the page 300 ms later and sees the old one. Whether that is a bug depends on the store’s consistency model: The contract between a replicated store and its clients that says which values a read may return, given the writes that have happened. It decides whether a stale read is a bug or allowed.: the contract between a replicated data store and the programs that use it, which says what a read may return, given the writes that have happened. With copies on several machines, a read can reach one that has not yet received the latest write. Strong models, with linearisable at the top, make the copies behave like one and pay for it with coordination on every operation and with errors during a network partition: A fault that splits a system's machines into groups that cannot exchange messages, while each group keeps running and serving the clients that can reach it. More in “CAP and PACELC, stated correctly”. Weak models, with eventual consistency at the bottom, let any copy answer at once and promise only that the copies converge.
Sequential and causal consistency and the session guarantees sit between the two. Below, a checker tests six histories against the two strongest models, a chat holds a reply back for 25 ms until its question arrives, and a table picks the weakest model for each operation.
A history is all a client can see
Client A writes 1 to a register and gets its answer at 2 ms; client B reads the same register at 3 ms and gets 0. Is that allowed? A client cannot watch the replicas. It sees only its own operations: when it sent a request (the invocation), and what came back and when (the response). A consistency model is therefore defined over a history (of operations): The list of every client's operations on an object, each with the time it was invoked, the time it returned and what it wrote or read. Consistency models are rules about which histories are legal., the list of every client’s operations with their invocation and response times, and it says which histories are legal. Step through that one as the two clients see it:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A invokes a write. At 0 ms client A sends a write of 1. Until the response comes back the write is in progress, and it may take effect at any instant in that interval.
- The write returns. At 2 ms the store answers ok. The write has returned, so it took effect somewhere between 0 and 2 ms.
- B invokes a read. At 3 ms client B sends a read. It starts after A's write returned, so the two are ordered in real time, the write first.
- B gets 0. At 4 ms the store answers 0, the value from before A's write. In the lesson's notation the history is A.w1 0-2, B.r0 3-4.
- Bug or allowed?. Neither client saw a replica, only these four messages. Whether the history is legal is decided by the consistency model, and the next two sections give two different answers.
Two operations either overlap in time, which makes them concurrent, or one of them returned before the other was
invoked, which orders them in real time. Above, B’s read started after A’s write returned, so real time puts the
write first. The examples in this lesson use the smallest object worth studying, a single register (one key) whose
value starts at 0. A write or a read is written A.w1 (client A writes 1) or B.r0 (client B reads 0), followed by
its times: A.w1 0-2 was invoked at 0 ms and returned at 2 ms. The same definitions apply to a key in a key-value
store, a row in a database or a whole account balance.
Checkpoint
Check: Histories of invocations and responses
Linearisable: one copy, in real time
linearisability: The strongest consistency model for one object: every operation appears to take effect at one instant between its invocation and its response, so all clients agree on one order that matches real time., defined by Herlihy and Wing in 1990, asks that every operation appear to take effect at a single instant between its invocation and its response (Herlihy and Wing). The instants put all operations into one order, every client agrees on it, and it never contradicts real time: if one operation returned before another was invoked, it comes first. Two consequences are worth remembering. Once a write has returned, every read that starts afterwards sees it or something newer. And once any read has returned the new value, no later read may return the old one, even on another client.
The definition can be checked by brute force on small histories: try the orders of the operations that respect real time, and see whether in one of them each read returns the latest write before it. The checker below does that for six histories, and also checks the weaker sequential consistency of the next section. Each tab is the same program in another language, and all 14 printed the lines under the tabs:
Python 3.14 · main.py
# A.w1 0-2: client A writes 1, invoked at 0 ms and returned at 2 ms; the register starts at 0.
# So op[0] is the client, op[2] w or r, op[3] the value, op[5] and op[7] the times.
from collections.abc import Callable
HISTORIES = [
"A.w1 0-2, B.r1 3-4",
"A.w1 0-2, B.r0 3-4",
"A.w1 0-6, B.r0 1-2, C.r1 3-5",
"A.w1 0-6, B.r1 1-2, C.r0 3-4",
"A.w1 0-1, A.r0 2-3",
"A.w1 0-1, B.w2 0-1, C.r1 2-3, C.r2 4-5, D.r2 2-3, D.r1 4-5",
]
Rule = Callable[[str, str], bool] # whether operation a must come before operation b
MODELS: list[tuple[str, Rule]] = [
("linearisable: ", lambda a, b: a[7] < b[5]), # a returned before b was invoked
("sequential: ", lambda a, b: a[0] == b[0] and a[5] < b[5]), # same client
]
def order(ops: list[str], first: Rule, value: str = "0") -> list[str] | None:
"""An order that keeps the rule, in which each read returns the latest write."""
if not ops:
return []
for op in ops:
rest = [o for o in ops if o != op]
if any(first(o, op) for o in rest) or (op[2] == "r" and op[3] != value):
continue
tail = order(rest, first, op[3])
if tail is not None:
return [op[:4], *tail]
return None
for n, history in enumerate(HISTORIES, 1):
print(f"{n}. {history}")
for label, first in MODELS:
found = order(history.split(", "), first)
print(" " + label + ("no" if found is None else " -> ".join(found))) In Python: An operation is its notation, so op[3] is the value and op[5] and op[7] the times, and the two models are lambdas in a list of pairs.
Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py
JavaScript Node 24 · main.mjs
// A.w1 0-2: client A writes 1, invoked at 0 ms and returned at 2 ms; the register starts at 0.
// So op[0] is the client, op[2] w or r, op[3] the value, op[5] and op[7] the times.
const HISTORIES = [
"A.w1 0-2, B.r1 3-4",
"A.w1 0-2, B.r0 3-4",
"A.w1 0-6, B.r0 1-2, C.r1 3-5",
"A.w1 0-6, B.r1 1-2, C.r0 3-4",
"A.w1 0-1, A.r0 2-3",
"A.w1 0-1, B.w2 0-1, C.r1 2-3, C.r2 4-5, D.r2 2-3, D.r1 4-5",
];
// Whether operation a must come before operation b.
const MODELS = [
["linearisable: ", (a, b) => a[7] < b[5]], // a returned before b was invoked
["sequential: ", (a, b) => a[0] === b[0] && a[5] < b[5]], // same client
];
// An order that keeps the rule, in which each read returns the latest write, or null.
function order(ops, first, value = "0") {
if (ops.length === 0) return [];
for (const op of ops) {
const rest = ops.filter((o) => o !== op);
if (rest.some((o) => first(o, op)) || (op[2] === "r" && op[3] !== value)) continue;
const tail = order(rest, first, op[3]);
if (tail !== null) return [op.slice(0, 4), ...tail];
}
return null;
}
HISTORIES.forEach((history, i) => {
console.log(`${i + 1}. ${history}`);
for (const [label, first] of MODELS) {
const found = order(history.split(", "), first);
console.log(` ${label}${found === null ? "no" : found.join(" -> ")}`);
}
}); In JavaScript: The rules compare one-character strings, which order like the digits they hold, and order returns null when no order exists.
Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs
Java 25 LTS · Main.java
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiPredicate;
public class Main {
// A.w1 0-2: client A writes 1, invoked at 0 ms and returned at 2 ms; the register starts at 0.
// So op.charAt(0) is the client, 2 is w or r, 3 the value, 5 and 7 the times.
static final String[] HISTORIES = {
"A.w1 0-2, B.r1 3-4",
"A.w1 0-2, B.r0 3-4",
"A.w1 0-6, B.r0 1-2, C.r1 3-5",
"A.w1 0-6, B.r1 1-2, C.r0 3-4",
"A.w1 0-1, A.r0 2-3",
"A.w1 0-1, B.w2 0-1, C.r1 2-3, C.r2 4-5, D.r2 2-3, D.r1 4-5",
};
// first.test(a, b): whether operation a must come before operation b.
record Model(String label, BiPredicate<String, String> first) {}
static final List<Model> MODELS =
List.of(
// a returned before b was invoked
new Model("linearisable: ", (a, b) -> a.charAt(7) < b.charAt(5)),
// the same client invoked a first
new Model(
"sequential: ", (a, b) -> a.charAt(0) == b.charAt(0) && a.charAt(5) < b.charAt(5)));
// An order that keeps the rule, in which each read returns the latest write, or null.
static List<String> order(List<String> ops, BiPredicate<String, String> first, char value) {
if (ops.isEmpty()) {
return new ArrayList<>();
}
for (String op : ops) {
List<String> rest = ops.stream().filter(o -> !o.equals(op)).toList();
if (rest.stream().anyMatch(o -> first.test(o, op))
|| (op.charAt(2) == 'r' && op.charAt(3) != value)) {
continue;
}
List<String> tail = order(rest, first, op.charAt(3));
if (tail != null) {
tail.add(0, op.substring(0, 4));
return tail;
}
}
return null;
}
public static void main(String[] args) {
for (int n = 0; n < HISTORIES.length; n++) {
System.out.println((n + 1) + ". " + HISTORIES[n]);
for (Model model : MODELS) {
List<String> found = order(List.of(HISTORIES[n].split(", ")), model.first(), '0');
System.out.println(
" " + model.label() + (found == null ? "no" : String.join(" -> ", found)));
}
}
}
} In Java: Each model is a record holding a BiPredicate, and charAt reads the client, the value and the times from the notation.
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java
C++23 · main.cpp
// A.w1 0-2: client A writes 1, invoked at 0 ms and returned at 2 ms; the
// register starts at 0. So op[0] is the client, op[2] w or r, op[3] the value,
// op[5] and op[7] the times.
#include <algorithm>
#include <functional>
#include <optional>
#include <print>
#include <ranges>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
using Ops = std::vector<std::string_view>;
// Whether operation a must come before operation b.
using Rule = std::function<bool(std::string_view a, std::string_view b)>;
// An order that keeps the rule, in which each read returns the latest write.
std::optional<Ops> order(const Ops &ops, const Rule &first, char value = '0') {
if (ops.empty())
return Ops{};
for (const std::string_view op : ops) {
Ops rest = ops;
std::erase(rest, op);
if (std::ranges::any_of(rest, [&](auto o) { return first(o, op); }) ||
(op[2] == 'r' && op[3] != value))
continue;
if (auto tail = order(rest, first, op[3])) {
tail->insert(tail->begin(), op.substr(0, 4));
return tail;
}
}
return std::nullopt;
}
int main() {
const std::vector<std::string_view> histories = {
"A.w1 0-2, B.r1 3-4",
"A.w1 0-2, B.r0 3-4",
"A.w1 0-6, B.r0 1-2, C.r1 3-5",
"A.w1 0-6, B.r1 1-2, C.r0 3-4",
"A.w1 0-1, A.r0 2-3",
"A.w1 0-1, B.w2 0-1, C.r1 2-3, C.r2 4-5, D.r2 2-3, D.r1 4-5",
};
const std::vector<std::pair<std::string_view, Rule>> models = {
// a returned before b was invoked
{"linearisable: ", [](auto a, auto b) { return a[7] < b[5]; }},
// the same client invoked a first
{"sequential: ",
[](auto a, auto b) { return a[0] == b[0] && a[5] < b[5]; }}};
for (std::size_t n = 0; n < histories.size(); n++) {
std::println("{}. {}", n + 1, histories[n]);
Ops ops;
for (const auto part :
std::views::split(histories[n], std::string_view(", ")))
ops.emplace_back(part.begin(), part.end());
for (const auto &[label, first] : models) {
std::string line;
if (const auto found = order(ops, first))
for (const std::string_view name : *found)
line += (line.empty() ? "" : " -> ") + std::string(name);
std::println(" {}{}", label, line.empty() ? "no" : line);
}
}
} In C++: The operations are std::string_view slices of the history, std::views::split cuts it at ", ", and order returns std::optional.
Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main
C# 14 · .NET 10 · app.cs
// A.w1 0-2: client A writes 1, invoked at 0 ms and returned at 2 ms; the register starts at 0.
// So op[0] is the client, op[2] w or r, op[3] the value, op[5] and op[7] the times.
string[] histories =
[
"A.w1 0-2, B.r1 3-4",
"A.w1 0-2, B.r0 3-4",
"A.w1 0-6, B.r0 1-2, C.r1 3-5",
"A.w1 0-6, B.r1 1-2, C.r0 3-4",
"A.w1 0-1, A.r0 2-3",
"A.w1 0-1, B.w2 0-1, C.r1 2-3, C.r2 4-5, D.r2 2-3, D.r1 4-5",
];
// Whether operation a must come before operation b.
(string Label, Func<string, string, bool> First)[] models =
[
("linearisable: ", (a, b) => a[7] < b[5]), // a returned before b was invoked
("sequential: ", (a, b) => a[0] == b[0] && a[5] < b[5]), // same client
];
for (int n = 0; n < histories.Length; n++)
{
Console.WriteLine($"{n + 1}. {histories[n]}");
foreach (var (label, first) in models)
{
var found = Order([.. histories[n].Split(", ")], first, '0');
Console.WriteLine(" " + label + (found is null ? "no" : string.Join(" -> ", found)));
}
}
// An order that keeps the rule, in which each read returns the latest write, or null.
static List<string>? Order(List<string> ops, Func<string, string, bool> first, char value)
{
if (ops.Count == 0)
{
return [];
}
foreach (string op in ops)
{
List<string> rest = [.. ops.Where(o => o != op)];
if (rest.Any(o => first(o, op)) || (op[2] == 'r' && op[3] != value))
{
continue;
}
if (Order(rest, first, op[3]) is { } tail)
{
return [op[..4], .. tail];
}
}
return null;
} In C#: The models are tuples of a label and a Func, and the pattern is { } tail names the order only when one was found, because { } matches any value that is not null.
Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs
C23 · main.c
// A.w1 0-2: client A writes 1, invoked at 0 ms and returned at 2 ms; the
// register starts at 0. So op[0] is the client, op[2] w or r, op[3] the value,
// op[5] and op[7] the times.
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
static const char *const histories[] = {
"A.w1 0-2, B.r1 3-4",
"A.w1 0-2, B.r0 3-4",
"A.w1 0-6, B.r0 1-2, C.r1 3-5",
"A.w1 0-6, B.r1 1-2, C.r0 3-4",
"A.w1 0-1, A.r0 2-3",
"A.w1 0-1, B.w2 0-1, C.r1 2-3, C.r2 4-5, D.r2 2-3, D.r1 4-5"};
// Whether operation a must come before operation b.
typedef bool Rule(const char *a, const char *b);
static bool real_time(const char *a, const char *b) { return a[7] < b[5]; }
static bool same_client(const char *a, const char *b) {
return a[0] == b[0] && a[5] < b[5];
}
// Puts into found[depth..] an order of the operations not in used that keeps
// the rule, in which each read returns the latest write.
static bool order(const char **ops, int n, Rule *first, unsigned used,
int depth, char value, const char **found) {
if (depth == n)
return true;
for (int i = 0; i < n; i++) {
bool waits = used >> i & 1 || (ops[i][2] == 'r' && ops[i][3] != value);
for (int j = 0; j < n && !waits; j++)
waits = j != i && !(used >> j & 1) && first(ops[j], ops[i]);
found[depth] = ops[i];
if (!waits &&
order(ops, n, first, used | 1u << i, depth + 1, ops[i][3], found))
return true;
}
return false;
}
static void show(const char *label, const char **ops, int n, Rule *first) {
const char *found[6];
printf(" %s", label);
if (!order(ops, n, first, 0, 0, '0', found)) {
printf("no\n");
return;
}
for (int i = 0; i < n; i++)
printf("%s%.4s", i ? " -> " : "", found[i]);
printf("\n");
}
int main(void) {
for (int h = 0; h < 6; h++) {
printf("%d. %s\n", h + 1, histories[h]);
const char *ops[6];
int n = 0;
for (const char *p = histories[h]; p; p = strchr(p, ',')) {
if (*p == ',')
p += 2;
ops[n++] = p;
}
show("linearisable: ", ops, n, real_time);
show("sequential: ", ops, n, same_client);
}
} In C: Each operation is a pointer into its history string, used marks the operations placed as bits, and found collects the order.
Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main
PHP 8.5 · main.php
<?php
declare(strict_types=1);
// A.w1 0-2: client A writes 1, invoked at 0 ms and returned at 2 ms; the register starts at 0.
// So $op[0] is the client, $op[2] w or r, $op[3] the value, $op[5] and $op[7] the times.
const HISTORIES = [
'A.w1 0-2, B.r1 3-4',
'A.w1 0-2, B.r0 3-4',
'A.w1 0-6, B.r0 1-2, C.r1 3-5',
'A.w1 0-6, B.r1 1-2, C.r0 3-4',
'A.w1 0-1, A.r0 2-3',
'A.w1 0-1, B.w2 0-1, C.r1 2-3, C.r2 4-5, D.r2 2-3, D.r1 4-5',
];
/**
* An order that keeps the rule, in which each read returns the latest write, or null.
*
* @param list<string> $ops
* @param callable(string, string): bool $first
* @return list<string>|null
*/
function order(array $ops, callable $first, string $value = '0'): ?array
{
if ($ops === []) {
return [];
}
foreach ($ops as $op) {
$rest = array_values(array_filter($ops, fn(string $o): bool => $o !== $op));
$waits = array_filter($rest, fn(string $o): bool => $first($o, $op)) !== [];
if ($waits || ($op[2] === 'r' && $op[3] !== $value)) {
continue;
}
$tail = order($rest, $first, $op[3]);
if ($tail !== null) {
return [substr($op, 0, 4), ...$tail];
}
}
return null;
}
// Whether operation $a must come before operation $b.
$models = [
// a returned before b was invoked
'linearisable: ' => fn(string $a, string $b): bool => $a[7] < $b[5],
// the same client invoked a first
'sequential: ' => fn(string $a, string $b): bool => $a[0] === $b[0] && $a[5] < $b[5],
];
foreach (HISTORIES as $n => $history) {
echo ($n + 1) . ". {$history}\n";
foreach ($models as $label => $first) {
$found = order(explode(', ', $history), $first);
echo " {$label}" . ($found === null ? 'no' : implode(' -> ', $found)) . "\n";
}
} In PHP: The models are arrow functions keyed by their label, and $a[7] < $b[5] compares numeric strings as numbers.
Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php
Rust 1.99 · main.rs
// A.w1 0-2: client A writes 1, invoked at 0 ms and returned at 2 ms; the register starts at 0.
// So op[0] is the client, op[2] w or r, op[3] the value, op[5] and op[7] the times.
const HISTORIES: [&str; 6] = [
"A.w1 0-2, B.r1 3-4",
"A.w1 0-2, B.r0 3-4",
"A.w1 0-6, B.r0 1-2, C.r1 3-5",
"A.w1 0-6, B.r1 1-2, C.r0 3-4",
"A.w1 0-1, A.r0 2-3",
"A.w1 0-1, B.w2 0-1, C.r1 2-3, C.r2 4-5, D.r2 2-3, D.r1 4-5",
];
/// Whether operation a must come before operation b.
type Rule = fn(&[u8], &[u8]) -> bool;
const MODELS: [(&str, Rule); 2] = [
("linearisable: ", |a, b| a[7] < b[5]), // a returned before b was invoked
("sequential: ", |a, b| a[0] == b[0] && a[5] < b[5]), // same client
];
/// An order that keeps the rule, in which each read returns the latest write.
fn order<'a>(ops: &[&'a str], first: Rule, value: u8) -> Option<Vec<&'a str>> {
if ops.is_empty() {
return Some(Vec::new());
}
for &op in ops {
let rest: Vec<&str> = ops.iter().copied().filter(|&o| o != op).collect();
let b = op.as_bytes();
if rest.iter().any(|o| first(o.as_bytes(), b)) || (b[2] == b'r' && b[3] != value) {
continue;
}
if let Some(mut tail) = order(&rest, first, b[3]) {
tail.insert(0, &op[..4]);
return Some(tail);
}
}
None
}
fn main() {
for (n, history) in HISTORIES.iter().enumerate() {
println!("{}. {history}", n + 1);
let ops: Vec<&str> = history.split(", ").collect();
for (label, first) in MODELS {
let found = order(&ops, first, b'0');
println!(
" {label}{}",
found.map_or("no".to_string(), |names| names.join(" -> "))
);
}
}
} In Rust: The rules are fn pointers over the notation's bytes, built from closures in a const array, and order returns an Option.
Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main
TypeScript 6.0 · main.ts
// A.w1 0-2: client A writes 1, invoked at 0 ms and returned at 2 ms; the register starts at 0.
// So op[0] is the client, op[2] w or r, op[3] the value, op[5] and op[7] the times.
const HISTORIES = [
"A.w1 0-2, B.r1 3-4",
"A.w1 0-2, B.r0 3-4",
"A.w1 0-6, B.r0 1-2, C.r1 3-5",
"A.w1 0-6, B.r1 1-2, C.r0 3-4",
"A.w1 0-1, A.r0 2-3",
"A.w1 0-1, B.w2 0-1, C.r1 2-3, C.r2 4-5, D.r2 2-3, D.r1 4-5",
];
type Rule = (a: string, b: string) => boolean; // whether operation a must come before b
const MODELS: [string, Rule][] = [
["linearisable: ", (a, b) => a[7] < b[5]], // a returned before b was invoked
["sequential: ", (a, b) => a[0] === b[0] && a[5] < b[5]], // same client
];
// An order that keeps the rule, in which each read returns the latest write, or null.
function order(ops: string[], first: Rule, value = "0"): string[] | null {
if (ops.length === 0) return [];
for (const op of ops) {
const rest = ops.filter((o) => o !== op);
if (rest.some((o) => first(o, op)) || (op[2] === "r" && op[3] !== value)) continue;
const tail = order(rest, first, op[3]);
if (tail !== null) return [op.slice(0, 4), ...tail];
}
return null;
}
HISTORIES.forEach((history, i) => {
console.log(`${i + 1}. ${history}`);
for (const [label, first] of MODELS) {
const found = order(history.split(", "), first);
console.log(` ${label}${found === null ? "no" : found.join(" -> ")}`);
}
}); In TypeScript: Rule is a function type, so both lambdas in MODELS are checked against it, and order returns null when no order exists.
Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts
Go 1.27 · main.go
// A.w1 0-2: client A writes 1, invoked at 0 ms and returned at 2 ms; the register starts at 0.
// So op[0] is the client, op[2] w or r, op[3] the value, op[5] and op[7] the times.
package main
import (
"fmt"
"slices"
"strings"
)
var histories = []string{
"A.w1 0-2, B.r1 3-4",
"A.w1 0-2, B.r0 3-4",
"A.w1 0-6, B.r0 1-2, C.r1 3-5",
"A.w1 0-6, B.r1 1-2, C.r0 3-4",
"A.w1 0-1, A.r0 2-3",
"A.w1 0-1, B.w2 0-1, C.r1 2-3, C.r2 4-5, D.r2 2-3, D.r1 4-5",
}
// A rule says whether operation a must come before operation b.
type rule func(a, b string) bool
var models = []struct {
label string
first rule
}{
// a returned before b was invoked
{"linearisable: ", func(a, b string) bool { return a[7] < b[5] }},
// the same client invoked a first
{"sequential: ", func(a, b string) bool { return a[0] == b[0] && a[5] < b[5] }},
}
// order finds an order that keeps the rule, in which each read returns the latest write.
func order(ops []string, first rule, value byte) ([]string, bool) {
if len(ops) == 0 {
return nil, true
}
for i, op := range ops {
rest := slices.Delete(slices.Clone(ops), i, i+1)
waits := op[2] == 'r' && op[3] != value
for _, o := range rest {
waits = waits || first(o, op)
}
if waits {
continue
}
if tail, ok := order(rest, first, op[3]); ok {
return append([]string{op[:4]}, tail...), true
}
}
return nil, false
}
func main() {
for n, history := range histories {
fmt.Printf("%d. %s\n", n+1, history)
for _, m := range models {
found, ok := order(strings.Split(history, ", "), m.first, '0')
result := strings.Join(found, " -> ")
if !ok {
result = "no"
}
fmt.Println(" " + m.label + result)
}
}
} In Go: order returns the names and a flag, the Go way of saying "found", and slices.Delete on a clone leaves the others.
Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go
Swift 6.4 · main.swift
// A.w1 0-2: client A writes 1, invoked at 0 ms and returned at 2 ms; the register starts at 0.
// So op[0] is the client, op[2] w or r, op[3] the value, op[5] and op[7] the times.
let histories = [
"A.w1 0-2, B.r1 3-4",
"A.w1 0-2, B.r0 3-4",
"A.w1 0-6, B.r0 1-2, C.r1 3-5",
"A.w1 0-6, B.r1 1-2, C.r0 3-4",
"A.w1 0-1, A.r0 2-3",
"A.w1 0-1, B.w2 0-1, C.r1 2-3, C.r2 4-5, D.r2 2-3, D.r1 4-5",
]
/// Whether operation a must come before operation b, each given as its notation's bytes.
typealias Rule = ([UInt8], [UInt8]) -> Bool
let models: [(label: String, first: Rule)] = [
("linearisable: ", { a, b in a[7] < b[5] }), // a returned before b was invoked
("sequential: ", { a, b in a[0] == b[0] && a[5] < b[5] }), // same client
]
/// An order that keeps the rule, in which each read returns the latest write.
func order(_ ops: [[UInt8]], _ first: Rule, _ value: UInt8 = UInt8(ascii: "0")) -> [String]? {
if ops.isEmpty {
return []
}
for (i, op) in ops.enumerated() {
var rest = ops
rest.remove(at: i)
if rest.contains(where: { first($0, op) }) || (op[2] == UInt8(ascii: "r") && op[3] != value) {
continue
}
if let tail = order(rest, first, op[3]) {
return [String(decoding: op[..<4], as: UTF8.self)] + tail
}
}
return nil
}
for (n, history) in histories.enumerated() {
print("\(n + 1). \(history)")
let ops = history.split(separator: ",").map { item in Array(item.drop(while: { $0 == " " }).utf8)
}
for (label, first) in models {
print(" " + label + (order(ops, first)?.joined(separator: " -> ") ?? "no"))
}
} In Swift: Swift strings have no integer index, so each operation is kept as the bytes of its notation, and order returns an optional.
Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main
Ruby 4.0 · main.rb
# frozen_string_literal: true
# A.w1 0-2: client A writes 1, invoked at 0 ms and returned at 2 ms; the register starts at 0.
# So op[0] is the client, op[2] w or r, op[3] the value, op[5] and op[7] the times.
HISTORIES = [
'A.w1 0-2, B.r1 3-4',
'A.w1 0-2, B.r0 3-4',
'A.w1 0-6, B.r0 1-2, C.r1 3-5',
'A.w1 0-6, B.r1 1-2, C.r0 3-4',
'A.w1 0-1, A.r0 2-3',
'A.w1 0-1, B.w2 0-1, C.r1 2-3, C.r2 4-5, D.r2 2-3, D.r1 4-5'
].freeze
# Whether operation a must come before operation b.
MODELS = {
'linearisable: ' => ->(a, b) { a[7] < b[5] }, # a returned before b was invoked
'sequential: ' => ->(a, b) { a[0] == b[0] && a[5] < b[5] } # same client
}.freeze
# Whether an operation may come next: nothing it must follow is left, and a read is current.
def ready?(operation, rest, first, value)
rest.none? { |o| first.call(o, operation) } && (operation[2] == 'w' || operation[3] == value)
end
# An order that keeps the rule, in which each read returns the latest write, or nil.
def order(ops, first, value = '0')
return [] if ops.empty?
ops.each do |op|
rest = ops - [op]
next unless ready?(op, rest, first, value)
tail = order(rest, first, op[3])
return [op[0, 4], *tail] if tail
end
nil
end
HISTORIES.each.with_index(1) do |history, n|
puts "#{n}. #{history}"
MODELS.each do |label, first|
found = order(history.split(', '), first)
puts " #{label}#{found ? found.join(' -> ') : 'no'}"
end
end In Ruby: RuboCop keeps methods short, so ready? says whether an operation may come next, and the models are lambdas in a hash.
Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb
Kotlin 2.4 · Main.kt
// A.w1 0-2: client A writes 1, invoked at 0 ms and returned at 2 ms; the register starts at 0.
// So op[0] is the client, op[2] w or r, op[3] the value, op[5] and op[7] the times.
val histories =
listOf(
"A.w1 0-2, B.r1 3-4",
"A.w1 0-2, B.r0 3-4",
"A.w1 0-6, B.r0 1-2, C.r1 3-5",
"A.w1 0-6, B.r1 1-2, C.r0 3-4",
"A.w1 0-1, A.r0 2-3",
"A.w1 0-1, B.w2 0-1, C.r1 2-3, C.r2 4-5, D.r2 2-3, D.r1 4-5",
)
// Whether operation a must come before operation b.
val models =
listOf<Pair<String, (String, String) -> Boolean>>(
"linearisable: " to { a, b -> a[7] < b[5] }, // a returned before b was invoked
"sequential: " to { a, b -> a[0] == b[0] && a[5] < b[5] }, // same client
)
// An order that keeps the rule, in which each read returns the latest write, or null.
fun order(
ops: List<String>,
first: (String, String) -> Boolean,
value: Char = '0',
): List<String>? {
if (ops.isEmpty()) return emptyList()
for (op in ops) {
val rest = ops - op
if (rest.any { first(it, op) } || (op[2] == 'r' && op[3] != value)) continue
val tail = order(rest, first, op[3]) ?: continue
return listOf(op.take(4)) + tail
}
return null
}
fun main() {
histories.forEachIndexed { n, history ->
println("${n + 1}. $history")
for ((label, first) in models) {
println(" $label${order(history.split(", "), first)?.joinToString(" -> ") ?: "no"}")
}
}
} In Kotlin: ops - op is a new list without the operation, and ?: continue moves on when no order follows it.
Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
Dart 3.13 · main.dart
// A.w1 0-2: client A writes 1, invoked at 0 ms and returned at 2 ms; the register starts at 0.
// So op[0] is the client, op[2] w or r, op[3] the value, op[5] and op[7] the times.
const histories = [
'A.w1 0-2, B.r1 3-4',
'A.w1 0-2, B.r0 3-4',
'A.w1 0-6, B.r0 1-2, C.r1 3-5',
'A.w1 0-6, B.r1 1-2, C.r0 3-4',
'A.w1 0-1, A.r0 2-3',
'A.w1 0-1, B.w2 0-1, C.r1 2-3, C.r2 4-5, D.r2 2-3, D.r1 4-5',
];
/// Whether operation a must come before operation b.
typedef Rule = bool Function(String a, String b);
final List<(String, Rule)> models = [
// a returned before b was invoked
('linearisable: ', (a, b) => a.codeUnitAt(7) < b.codeUnitAt(5)),
// the same client invoked a first
(
'sequential: ',
(a, b) => a[0] == b[0] && a.codeUnitAt(5) < b.codeUnitAt(5),
),
];
/// An order that keeps the rule, in which each read returns the latest write, or null.
List<String>? order(List<String> ops, Rule first, [String value = '0']) {
if (ops.isEmpty) return [];
for (final op in ops) {
final rest = [...ops]..remove(op);
if (rest.any((o) => first(o, op)) || (op[2] == 'r' && op[3] != value))
continue;
final tail = order(rest, first, op[3]);
if (tail != null) return [op.substring(0, 4), ...tail];
}
return null;
}
void main() {
for (final (n, history) in histories.indexed) {
print('${n + 1}. $history');
for (final (label, first) in models) {
print(
' $label${order(history.split(', '), first)?.join(' -> ') ?? 'no'}',
);
}
}
} In Dart: Dart strings cannot be compared with <, so the rules compare code units, and order returns null when no order exists.
Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart
What every language printed
1. A.w1 0-2, B.r1 3-4 linearisable: A.w1 -> B.r1 sequential: A.w1 -> B.r1 2. A.w1 0-2, B.r0 3-4 linearisable: no sequential: B.r0 -> A.w1 3. A.w1 0-6, B.r0 1-2, C.r1 3-5 linearisable: B.r0 -> A.w1 -> C.r1 sequential: B.r0 -> A.w1 -> C.r1 4. A.w1 0-6, B.r1 1-2, C.r0 3-4 linearisable: no sequential: C.r0 -> A.w1 -> B.r1 5. A.w1 0-1, A.r0 2-3 linearisable: no sequential: no 6. A.w1 0-1, B.w2 0-1, C.r1 2-3, C.r2 4-5, D.r2 2-3, D.r1 4-5 linearisable: no sequential: no
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 49 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 153 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 4 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 9 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 26 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 723 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 186 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 16 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
Histories 1 and 3 are linearisable. In history 3 the write is slow (it runs from 0 to 6 ms), so it may take effect at any instant inside that interval: B’s read finished before that instant and saw 0, and C’s read started late enough to see 1. Histories 2 and 4 are not. In history 2 the write had returned at 2 ms, so a read invoked at 3 ms must not see the old value. History 4 is the subtle one: no single read is stale by itself, yet once B has seen 1, C, which starts after B finished, sees 0 again. Step through it:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A slow write. At 0 ms client A invokes a write of 1, which returns only at 6 ms. A linearisable store must give the write one instant somewhere in that interval.
- B reads. B's read runs from 1 to 2 ms, inside the write's interval.
- B sees 1. B gets 1. So the write's instant came before B's read ended, at 2 ms at the latest.
- C reads later. C's read starts at 3 ms, after B's read returned. In real time it comes after B's read, so it must see 1 as well.
- C sees 0. C gets 0, the old value. The write cannot take effect both by 2 ms, for B, and after 4 ms, for C, so no single order fits real time.
- Only sequential allows it. Sequential consistency drops real time between clients. The order C.r0, then A.w1, then B.r1 keeps each client's own order, so the checker accepts it.
A linearisable store never flickers back like that. The price is coordination. A replica cannot answer a linearisable read from its own copy, because it cannot know alone that no newer write has taken effect elsewhere. Systems pay this with a single leader that checks it still leads before answering, or with reads and writes that each reach a majority of the replicas. During a network partition, the replicas on the side without a majority cannot take part, so a linearisable store cannot be fully available on both sides of a partition (Jepsen).
Key idea
Linearisable means one order of all operations that every client agrees on and that matches real time: once a write, or a read of the new value, has returned, no later read returns the old value.
Checkpoint
Check: Linearisability
Sequential: one order, but possibly late
sequential consistency: A consistency model in which all operations appear in one order that every client agrees on and that keeps each client's own operations in the order it issued them, though that order may lag behind real time., defined by Lamport in 1979 for multiprocessor memory, keeps the single agreed order but drops real time between clients: the result must be as if all operations ran one at a time in some order, with each client’s own operations in the order that client issued them (Lamport). A client may therefore be behind the others, and read a value that was overwritten long ago by wall-clock time, as long as everyone agrees on one order and nobody’s own operations appear out of order. Here is what the checker found under both models:
| History | Linearisable | Sequential |
|---|---|---|
| 1. read after the write | yes: A.w1 → B.r1 | yes |
| 2. stale read after the write | no | yes: B.r0 → A.w1 |
| 3. reads during a slow write | yes: B.r0 → A.w1 → C.r1 | yes |
| 4. new value, then the old one | no | yes: C.r0 → A.w1 → B.r1 |
| 5. a process misses its write | no | no |
| 6. two writes, two orders | no | no |
Histories 2 and 4 are sequentially consistent: put the stale read before the write and every rule holds. What sequential consistency still forbids is history 5, in which a client writes 1 and then reads 0, because its own operations would appear out of order, and history 6, in which client C sees the write of 1 before the write of 2 while client D sees them the other way round, because there is no single order both could agree with. It has a cost too: like linearisability, it cannot be kept by replicas that have lost contact with the rest (Jepsen).
Reading sequential as almost linearisable
“Sequentially consistent, so a write is visible once it has returned” is wrong: history 2 is sequentially consistent, and its read 1 ms after the write returned still saw 0. The model promises one agreed order and each client’s own order, nothing about how far behind the clock that order may run.
Checkpoint
Check: Sequential consistency
Causal: every effect after its cause
causal consistency: A consistency model in which causally related operations appear in the same order everywhere, while operations made without knowledge of each other may appear in different orders on different replicas. gives up the single order and keeps only the order that matters to people: operations that are causally related appear in the same order everywhere, and operations that are not may appear in different orders on different replicas (Jepsen). One operation causes another if the second was issued later by the same client, or by a client that had already seen the first, directly or through a chain of such steps. A reply in a chat is caused by the question it answers, so every replica must show the question first. Two messages written without knowledge of each other are concurrent, and replicas may show them in either order.
The program below runs a three-person chat on three replicas whose links have different delays. Asha asks a question on R1, Ben reads it on R2 and replies, and Chen writes something unrelated on R3. Then it runs the same messages with causal delivery: each message carries a vector clock: One counter per process, carried with every message, counting the events of each process that the sender knows of. Comparing two vector clocks shows whether one event could have caused the other or whether they are concurrent. More in “Clocks and ordering: Lamport, vector and HLC”, a count of the messages from each replica that its author had seen, and a replica holds a message back until it has shown everything that message depends on.
Python 3.14 · main.py
# Post p is written on replica p, with a vector clock: the posts of each replica its author saw.
SENT_MS = [0, 12, 14]
DELAY_MS = [
[0, 10, 40],
[10, 0, 3],
[16, 6, 0],
] # from the row's replica to the column's
CLOCKS = [[1, 0, 0], [1, 1, 0], [0, 0, 1]] # Ben had seen Asha's post, Chen neither
TEXTS = ["Asha: Lunch at 1?", "Ben: Yes!", "Chen: Late today"]
def ready(post: int, seen: list[int]) -> bool:
"""Whether a replica that has shown seen may show the post."""
return all(c <= s + (x == post) for x, (c, s) in enumerate(zip(CLOCKS[post], seen)))
def run(causal: bool) -> None:
seen = [[0, 0, 0] for _ in range(3)] # how many posts of each replica r has shown
held = [[False] * 3 for _ in range(3)]
shown = [[] for _ in range(3)]
for t, p, r in sorted(
(SENT_MS[p] + DELAY_MS[p][r], p, r) for p in range(3) for r in range(3)
):
held[r][p] = True
while free := [
q for q in range(3) if held[r][q] and (not causal or ready(q, seen[r]))
]:
held[r][free[0]] = False
shown[r].append(TEXTS[free[0]])
seen[r][free[0]] += 1
if held[r][p]:
print(f' R{r + 1} at {t} ms holds "{TEXTS[p]}"')
for r in range(3):
print(f" R{r + 1}: {' | '.join(shown[r])}")
print("as messages arrive:")
run(False)
print("with causal delivery:")
run(True) In Python: The arrivals are tuples, sorted by time, post and replica, and the walrus loop shows held posts while one may be shown.
Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py
JavaScript Node 24 · main.mjs
// Post p is written on replica p, with a vector clock: the posts of each replica its author saw.
const SENT_MS = [0, 12, 14];
const DELAY_MS = [
[0, 10, 40],
[10, 0, 3],
[16, 6, 0],
]; // from the row's replica to the column's
const CLOCKS = [
[1, 0, 0],
[1, 1, 0],
[0, 0, 1],
]; // Ben had seen Asha's post, Chen neither
const TEXTS = ["Asha: Lunch at 1?", "Ben: Yes!", "Chen: Late today"];
// Whether a replica that has shown seen may show the post.
const ready = (post, seen) => CLOCKS[post].every((c, x) => c <= seen[x] + (x === post ? 1 : 0));
function run(causal) {
const seen = [0, 1, 2].map(() => [0, 0, 0]); // how many posts of each replica r has shown
const held = [0, 1, 2].map(() => [false, false, false]);
const shown = [[], [], []];
const arrivals = [0, 1, 2].flatMap((p) =>
[0, 1, 2].map((r) => [SENT_MS[p] + DELAY_MS[p][r], p, r]),
);
arrivals.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2]);
for (const [t, p, r] of arrivals) {
held[r][p] = true;
let q;
while (
(q = [0, 1, 2].find((x) => held[r][x] && (!causal || ready(x, seen[r])))) !== undefined
) {
held[r][q] = false;
shown[r].push(TEXTS[q]);
seen[r][q] += 1;
}
if (held[r][p]) console.log(` R${r + 1} at ${t} ms holds "${TEXTS[p]}"`);
}
shown.forEach((list, r) => console.log(` R${r + 1}: ${list.join(" | ")}`));
}
console.log("as messages arrive:");
run(false);
console.log("with causal delivery:");
run(true); In JavaScript: find returns undefined when no held post may be shown, which ends the while loop.
Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs
Java 25 LTS · Main.java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.IntStream;
public class Main {
// Post p is written on replica p, with a vector clock: the posts of each replica its author saw.
static final int[] SENT_MS = {0, 12, 14};
// From the row's replica to the column's.
static final int[][] DELAY_MS = {{0, 10, 40}, {10, 0, 3}, {16, 6, 0}};
// Ben had seen Asha's post, Chen neither.
static final int[][] CLOCKS = {{1, 0, 0}, {1, 1, 0}, {0, 0, 1}};
static final String[] TEXTS = {"Asha: Lunch at 1?", "Ben: Yes!", "Chen: Late today"};
// Whether a replica that has shown seen may show the post.
static boolean ready(int post, int[] seen) {
return IntStream.range(0, 3).allMatch(x -> CLOCKS[post][x] <= seen[x] + (x == post ? 1 : 0));
}
static void run(boolean causal) {
int[][] seen = new int[3][3]; // how many posts of each replica r has shown
boolean[][] held = new boolean[3][3];
List<List<String>> shown = List.of(new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
List<int[]> arrivals = new ArrayList<>();
for (int p = 0; p < 3; p++) {
for (int r = 0; r < 3; r++) {
arrivals.add(new int[] {SENT_MS[p] + DELAY_MS[p][r], p, r});
}
}
arrivals.sort(
Comparator.<int[]>comparingInt(a -> a[0])
.thenComparingInt(a -> a[1])
.thenComparingInt(a -> a[2]));
for (int[] a : arrivals) {
int r = a[2];
held[r][a[1]] = true;
for (int q = 0; q < 3; q++) {
if (held[r][q] && (!causal || ready(q, seen[r]))) {
held[r][q] = false;
shown.get(r).add(TEXTS[q]);
seen[r][q]++;
q = -1; // look again from the first post
}
}
if (held[r][a[1]]) {
System.out.println(" R" + (r + 1) + " at " + a[0] + " ms holds \"" + TEXTS[a[1]] + "\"");
}
}
for (int r = 0; r < 3; r++) {
System.out.println(" R" + (r + 1) + ": " + String.join(" | ", shown.get(r)));
}
}
public static void main(String[] args) {
System.out.println("as messages arrive:");
run(false);
System.out.println("with causal delivery:");
run(true);
}
} In Java: A Comparator built with thenComparingInt sorts the arrivals, and setting q back to -1 starts the scan again from the first post.
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java
C++23 · main.cpp
// Post p is written on replica p, with a vector clock: the posts of each
// replica its author saw.
#include <algorithm>
#include <array>
#include <print>
#include <string>
#include <tuple>
#include <vector>
using Row = std::array<int, 3>;
constexpr Row sent_ms = {0, 12, 14};
// From the row's replica to the column's.
constexpr std::array<Row, 3> delay_ms = {{{0, 10, 40}, {10, 0, 3}, {16, 6, 0}}};
// Ben had seen Asha's post, Chen neither.
constexpr std::array<Row, 3> clocks = {{{1, 0, 0}, {1, 1, 0}, {0, 0, 1}}};
const std::array<std::string, 3> texts = {"Asha: Lunch at 1?", "Ben: Yes!",
"Chen: Late today"};
// Whether a replica that has shown seen may show the post.
bool ready(int post, const Row &seen) {
for (int x = 0; x < 3; x++)
if (clocks[post][x] > seen[x] + (x == post))
return false;
return true;
}
void run(bool causal) {
std::array<Row, 3> seen{}; // how many posts of each replica r has shown
std::array<std::array<bool, 3>, 3> held{};
std::array<std::string, 3> shown;
std::vector<std::tuple<int, int, int>> arrivals;
for (int p = 0; p < 3; p++)
for (int r = 0; r < 3; r++)
arrivals.emplace_back(sent_ms[p] + delay_ms[p][r], p, r);
std::ranges::sort(arrivals);
for (const auto &[t, p, r] : arrivals) {
held[r][p] = true;
for (int q = 0; q < 3; q++)
if (held[r][q] && (!causal || ready(q, seen[r]))) {
held[r][q] = false;
shown[r] += (shown[r].empty() ? "" : " | ") + texts[q];
seen[r][q]++;
q = -1; // look again from the first post
}
if (held[r][p])
std::println(" R{} at {} ms holds \"{}\"", r + 1, t, texts[p]);
}
for (int r = 0; r < 3; r++)
std::println(" R{}: {}", r + 1, shown[r]);
}
int main() {
std::println("as messages arrive:");
run(false);
std::println("with causal delivery:");
run(true);
} In C++: The arrivals are std::tuple values, which std::ranges::sort orders by time, post and replica, as the other languages do.
Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main
C# 14 · .NET 10 · app.cs
// Post p is written on replica p, with a vector clock: the posts of each replica its author saw.
int[] sentMs = [0, 12, 14];
int[][] delayMs = [[0, 10, 40], [10, 0, 3], [16, 6, 0]]; // from the row's replica to the column's
int[][] clocks = [[1, 0, 0], [1, 1, 0], [0, 0, 1]]; // Ben had seen Asha's post, Chen neither
string[] texts = ["Asha: Lunch at 1?", "Ben: Yes!", "Chen: Late today"];
Console.WriteLine("as messages arrive:");
Run(false);
Console.WriteLine("with causal delivery:");
Run(true);
// Whether a replica that has shown seen may show the post.
bool Ready(int post, int[] seen) =>
Enumerable.Range(0, 3).All(x => clocks[post][x] <= seen[x] + (x == post ? 1 : 0));
void Run(bool causal)
{
// How many posts of each replica r has shown.
int[][] seen = [new int[3], new int[3], new int[3]];
bool[][] held = [new bool[3], new bool[3], new bool[3]];
List<string>[] shown = [[], [], []];
var arrivals =
from p in Enumerable.Range(0, 3)
from r in Enumerable.Range(0, 3)
let t = sentMs[p] + delayMs[p][r]
orderby t, p, r
select (t, p, r);
foreach (var (t, p, r) in arrivals)
{
held[r][p] = true;
for (int q = 0; q < 3; q++)
{
if (held[r][q] && (!causal || Ready(q, seen[r])))
{
held[r][q] = false;
shown[r].Add(texts[q]);
seen[r][q]++;
q = -1; // look again from the first post
}
}
if (held[r][p])
{
Console.WriteLine($" R{r + 1} at {t} ms holds \"{texts[p]}\"");
}
}
for (int r = 0; r < 3; r++)
{
Console.WriteLine($" R{r + 1}: {string.Join(" | ", shown[r])}");
}
} In C#: A query expression builds and sorts the arrivals, with let naming the time once.
Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs
C23 · main.c
// Post p is written on replica p, with a vector clock: the posts of each
// replica its author saw.
#include <stdbool.h>
#include <stdio.h>
static const int sent_ms[3] = {0, 12, 14};
// From the row's replica to the column's.
static const int delay_ms[3][3] = {{0, 10, 40}, {10, 0, 3}, {16, 6, 0}};
// Ben had seen Asha's post, Chen neither.
static const int clocks[3][3] = {{1, 0, 0}, {1, 1, 0}, {0, 0, 1}};
static const char *const texts[3] = {"Asha: Lunch at 1?", "Ben: Yes!",
"Chen: Late today"};
// Whether a replica that has shown seen may show the post.
static bool ready(int post, const int seen[3]) {
for (int x = 0; x < 3; x++)
if (clocks[post][x] > seen[x] + (x == post))
return false;
return true;
}
static void run(bool causal) {
int seen[3][3] = {0}; // how many posts of each replica r has shown
bool held[3][3] = {0};
int shown[3][3], n_shown[3] = {0};
for (int t = 0; t < 50; t++) // the arrivals in order of time, post, replica
for (int p = 0; p < 3; p++)
for (int r = 0; r < 3; r++) {
if (sent_ms[p] + delay_ms[p][r] != t)
continue;
held[r][p] = true;
for (int q = 0; q < 3; q++)
if (held[r][q] && (!causal || ready(q, seen[r]))) {
held[r][q] = false;
shown[r][n_shown[r]++] = q;
seen[r][q]++;
q = -1; // look again from the first post
}
if (held[r][p])
printf(" R%d at %d ms holds \"%s\"\n", r + 1, t, texts[p]);
}
for (int r = 0; r < 3; r++) {
printf(" R%d:", r + 1);
for (int i = 0; i < n_shown[r]; i++)
printf("%s %s", i ? " |" : "", texts[shown[r][i]]);
printf("\n");
}
}
int main(void) {
printf("as messages arrive:\n");
run(false);
printf("with causal delivery:\n");
run(true);
} In C: The loop over every millisecond, post and replica visits the arrivals in the same order as the others sort them.
Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main
PHP 8.5 · main.php
<?php
declare(strict_types=1);
// Post p is written on replica p, with a vector clock: the posts of each replica its author saw.
const SENT_MS = [0, 12, 14];
const DELAY_MS = [[0, 10, 40], [10, 0, 3], [16, 6, 0]]; // from the row's replica to the column's
const CLOCKS = [[1, 0, 0], [1, 1, 0], [0, 0, 1]]; // Ben had seen Asha's post, Chen neither
const TEXTS = ['Asha: Lunch at 1?', 'Ben: Yes!', 'Chen: Late today'];
/**
* Whether a replica that has shown $seen may show the post.
*
* @param list<int> $seen
*/
function ready(int $post, array $seen): bool
{
foreach (CLOCKS[$post] as $x => $count) {
if ($count > $seen[$x] + ($x === $post ? 1 : 0)) {
return false;
}
}
return true;
}
function run(bool $causal): void
{
$seen = array_fill(0, 3, [0, 0, 0]); // how many posts of each replica r has shown
$held = array_fill(0, 3, [false, false, false]);
$shown = [[], [], []];
$arrivals = [];
foreach ([0, 1, 2] as $p) {
foreach ([0, 1, 2] as $r) {
$arrivals[] = [SENT_MS[$p] + DELAY_MS[$p][$r], $p, $r];
}
}
sort($arrivals);
foreach ($arrivals as [$t, $p, $r]) {
$held[$r][$p] = true;
for ($q = 0; $q < 3; $q++) {
if ($held[$r][$q] && (!$causal || ready($q, $seen[$r]))) {
$held[$r][$q] = false;
$shown[$r][] = TEXTS[$q];
$seen[$r][$q]++;
$q = -1; // look again from the first post
}
}
if ($held[$r][$p]) {
echo ' R' . ($r + 1) . " at {$t} ms holds \"" . TEXTS[$p] . "\"\n";
}
}
foreach ($shown as $r => $list) {
echo ' R' . ($r + 1) . ': ' . implode(' | ', $list) . "\n";
}
}
echo "as messages arrive:\n";
run(false);
echo "with causal delivery:\n";
run(true); In PHP: sort orders arrays item by item, so the arrivals sort by time, then post, then replica.
Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php
Rust 1.99 · main.rs
// Post p is written on replica p, with a vector clock: the posts of each replica its author saw.
const SENT_MS: [u32; 3] = [0, 12, 14];
// From the row's replica to the column's.
const DELAY_MS: [[u32; 3]; 3] = [[0, 10, 40], [10, 0, 3], [16, 6, 0]];
// Ben had seen Asha's post, Chen neither.
const CLOCKS: [[u32; 3]; 3] = [[1, 0, 0], [1, 1, 0], [0, 0, 1]];
const TEXTS: [&str; 3] = ["Asha: Lunch at 1?", "Ben: Yes!", "Chen: Late today"];
/// Whether a replica that has shown seen may show the post.
fn ready(post: usize, seen: &[u32; 3]) -> bool {
(0..3).all(|x| CLOCKS[post][x] <= seen[x] + u32::from(x == post))
}
fn run(causal: bool) {
let mut seen = [[0; 3]; 3]; // how many posts of each replica r has shown
let mut held = [[false; 3]; 3];
let mut shown: [Vec<&str>; 3] = Default::default();
let mut arrivals: Vec<(u32, usize, usize)> = (0..3)
.flat_map(|p| (0..3).map(move |r| (SENT_MS[p] + DELAY_MS[p][r], p, r)))
.collect();
arrivals.sort();
for (t, p, r) in arrivals {
held[r][p] = true;
while let Some(q) = (0..3).find(|&q| held[r][q] && (!causal || ready(q, &seen[r]))) {
held[r][q] = false;
shown[r].push(TEXTS[q]);
seen[r][q] += 1;
}
if held[r][p] {
println!(" R{} at {t} ms holds \"{}\"", r + 1, TEXTS[p]);
}
}
for (r, list) in shown.iter().enumerate() {
println!(" R{}: {}", r + 1, list.join(" | "));
}
}
fn main() {
println!("as messages arrive:");
run(false);
println!("with causal delivery:");
run(true);
} In Rust: Tuples compare item by item, so sort orders the arrivals, and while let with find shows held posts until none may be shown.
Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main
TypeScript 6.0 · main.ts
// Post p is written on replica p, with a vector clock: the posts of each replica its author saw.
const SENT_MS = [0, 12, 14];
const DELAY_MS = [
[0, 10, 40],
[10, 0, 3],
[16, 6, 0],
]; // from the row's replica to the column's
const CLOCKS = [
[1, 0, 0],
[1, 1, 0],
[0, 0, 1],
]; // Ben had seen Asha's post, Chen neither
const TEXTS = ["Asha: Lunch at 1?", "Ben: Yes!", "Chen: Late today"];
// Whether a replica that has shown seen may show the post.
const ready = (post: number, seen: number[]): boolean =>
CLOCKS[post].every((c, x) => c <= seen[x] + (x === post ? 1 : 0));
function run(causal: boolean): void {
const seen = [0, 1, 2].map(() => [0, 0, 0]); // how many posts of each replica r has shown
const held = [0, 1, 2].map(() => [false, false, false]);
const shown: string[][] = [[], [], []];
const arrivals = [0, 1, 2].flatMap((p) =>
[0, 1, 2].map((r) => [SENT_MS[p] + DELAY_MS[p][r], p, r]),
);
arrivals.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2]);
for (const [t, p, r] of arrivals) {
held[r][p] = true;
let q: number | undefined;
while (
(q = [0, 1, 2].find((x) => held[r][x] && (!causal || ready(x, seen[r])))) !== undefined
) {
held[r][q] = false;
shown[r].push(TEXTS[q]);
seen[r][q] += 1;
}
if (held[r][p]) console.log(` R${r + 1} at ${t} ms holds "${TEXTS[p]}"`);
}
shown.forEach((list, r) => console.log(` R${r + 1}: ${list.join(" | ")}`));
}
console.log("as messages arrive:");
run(false);
console.log("with causal delivery:");
run(true); In TypeScript: The arrivals are number arrays sorted with a comparator, and q is number | undefined until find has an answer.
Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts
Go 1.27 · main.go
// Post p is written on replica p, with a vector clock: the posts of each replica its author saw.
package main
import (
"fmt"
"slices"
"strings"
)
var (
sentMs = [3]int{0, 12, 14}
// From the row's replica to the column's.
delayMs = [3][3]int{{0, 10, 40}, {10, 0, 3}, {16, 6, 0}}
// Ben had seen Asha's post, Chen neither.
clocks = [3][3]int{{1, 0, 0}, {1, 1, 0}, {0, 0, 1}}
texts = [3]string{"Asha: Lunch at 1?", "Ben: Yes!", "Chen: Late today"}
)
// ready says whether a replica that has shown seen may show the post.
func ready(post int, seen [3]int) bool {
for x := range 3 {
own := 0
if x == post {
own = 1
}
if clocks[post][x] > seen[x]+own {
return false
}
}
return true
}
func run(causal bool) {
var seen [3][3]int // how many posts of each replica r has shown
var held [3][3]bool
var shown [3][]string
var arrivals [][3]int
for p := range 3 {
for r := range 3 {
arrivals = append(arrivals, [3]int{sentMs[p] + delayMs[p][r], p, r})
}
}
slices.SortFunc(arrivals, func(a, b [3]int) int { return slices.Compare(a[:], b[:]) })
for _, a := range arrivals {
t, p, r := a[0], a[1], a[2]
held[r][p] = true
for q := 0; q < 3; q++ {
if held[r][q] && (!causal || ready(q, seen[r])) {
held[r][q] = false
shown[r] = append(shown[r], texts[q])
seen[r][q]++
q = -1 // look again from the first post
}
}
if held[r][p] {
fmt.Printf(" R%d at %d ms holds %q\n", r+1, t, texts[p])
}
}
for r, list := range shown {
fmt.Printf(" R%d: %s\n", r+1, strings.Join(list, " | "))
}
}
func main() {
fmt.Println("as messages arrive:")
run(false)
fmt.Println("with causal delivery:")
run(true)
} In Go: slices.Compare orders two arrival arrays item by item, and %q prints the held post in quotes.
Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go
Swift 6.4 · main.swift
// Post p is written on replica p, with a vector clock: the posts of each replica its author saw.
let sentMs = [0, 12, 14]
let delayMs = [[0, 10, 40], [10, 0, 3], [16, 6, 0]] // from the row's replica to the column's
let clocks = [[1, 0, 0], [1, 1, 0], [0, 0, 1]] // Ben had seen Asha's post, Chen neither
let texts = ["Asha: Lunch at 1?", "Ben: Yes!", "Chen: Late today"]
/// Whether a replica that has shown seen may show the post.
func ready(_ post: Int, _ seen: [Int]) -> Bool {
(0..<3).allSatisfy { clocks[post][$0] <= seen[$0] + ($0 == post ? 1 : 0) }
}
func run(causal: Bool) {
var seen = Array(repeating: [0, 0, 0], count: 3) // how many posts of each replica r has shown
var held = Array(repeating: [false, false, false], count: 3)
var shown: [[String]] = [[], [], []]
let arrivals = (0..<3).flatMap { p in (0..<3).map { r in (sentMs[p] + delayMs[p][r], p, r) } }
for (t, p, r) in arrivals.sorted(by: { $0 < $1 }) {
held[r][p] = true
while let q = (0..<3).first(where: { held[r][$0] && (!causal || ready($0, seen[r])) }) {
held[r][q] = false
shown[r].append(texts[q])
seen[r][q] += 1
}
if held[r][p] {
print(" R\(r + 1) at \(t) ms holds \"\(texts[p])\"")
}
}
for (r, list) in shown.enumerated() {
print(" R\(r + 1): \(list.joined(separator: " | "))")
}
}
print("as messages arrive:")
run(causal: false)
print("with causal delivery:")
run(causal: true) In Swift: Tuples of comparable values compare with <, which sorts the arrivals, and while let with first(where:) shows held posts.
Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main
Ruby 4.0 · main.rb
# frozen_string_literal: true
# Post p is written on replica p, with a vector clock: the posts of each replica its author saw.
SENT_MS = [0, 12, 14].freeze
DELAY_MS = [[0, 10, 40], [10, 0, 3], [16, 6, 0]].freeze # from the row's replica to the column's
CLOCKS = [[1, 0, 0], [1, 1, 0], [0, 0, 1]].freeze # Ben had seen Asha's post, Chen neither
TEXTS = ['Asha: Lunch at 1?', 'Ben: Yes!', 'Chen: Late today'].freeze
# The posts arriving at three replicas, shown as they arrive or with causal delivery.
class Run
def initialize(causal)
@causal = causal
@seen = Array.new(3) { [0, 0, 0] } # how many posts of each replica r has shown
@held = Array.new(3) { [false, false, false] }
@shown = [[], [], []]
end
# Whether replica r may show the post now.
def ready?(post, replica)
@held[replica][post] && (!@causal || (0..2).all? do |x|
CLOCKS[post][x] <= @seen[replica][x] + (x == post ? 1 : 0)
end)
end
def arrive(time, post, replica)
@held[replica][post] = true
while (q = (0..2).find { |x| ready?(x, replica) })
show(q, replica)
end
puts %( R#{replica + 1} at #{time} ms holds "#{TEXTS[post]}") if @held[replica][post]
end
def show(post, replica)
@held[replica][post] = false
@shown[replica] << TEXTS[post]
@seen[replica][post] += 1
end
def play
(0..2).to_a.product((0..2).to_a).map { |p, r| [SENT_MS[p] + DELAY_MS[p][r], p, r] }.sort
.each { |time, post, replica| arrive(time, post, replica) }
@shown.each_with_index { |list, r| puts " R#{r + 1}: #{list.join(' | ')}" }
end
end
puts 'as messages arrive:'
Run.new(false).play
puts 'with causal delivery:'
Run.new(true).play In Ruby: RuboCop keeps methods short, so one Run object holds the state and arrive, show and play each take one step.
Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb
Kotlin 2.4 · Main.kt
// Post p is written on replica p, with a vector clock: the posts of each replica its author saw.
val sentMs = intArrayOf(0, 12, 14)
// From the row's replica to the column's.
val delayMs = listOf(intArrayOf(0, 10, 40), intArrayOf(10, 0, 3), intArrayOf(16, 6, 0))
// Ben had seen Asha's post, Chen neither.
val clocks = listOf(intArrayOf(1, 0, 0), intArrayOf(1, 1, 0), intArrayOf(0, 0, 1))
val texts = listOf("Asha: Lunch at 1?", "Ben: Yes!", "Chen: Late today")
// Whether a replica that has shown seen may show the post.
fun ready(
post: Int,
seen: IntArray,
) = (0..2).all { clocks[post][it] <= seen[it] + if (it == post) 1 else 0 }
fun run(causal: Boolean) {
val seen = List(3) { IntArray(3) } // how many posts of each replica r has shown
val held = List(3) { BooleanArray(3) }
val shown = List(3) { mutableListOf<String>() }
val arrivals =
(0..2)
.flatMap { p -> (0..2).map { r -> Triple(sentMs[p] + delayMs[p][r], p, r) } }
.sortedWith(compareBy({ it.first }, { it.second }, { it.third }))
for ((t, p, r) in arrivals) {
held[r][p] = true
while (true) {
val q = (0..2).firstOrNull { held[r][it] && (!causal || ready(it, seen[r])) } ?: break
held[r][q] = false
shown[r] += texts[q]
seen[r][q]++
}
if (held[r][p]) println(" R${r + 1} at $t ms holds \"${texts[p]}\"")
}
shown.forEachIndexed { r, list -> println(" R${r + 1}: ${list.joinToString(" | ")}") }
}
fun main() {
println("as messages arrive:")
run(false)
println("with causal delivery:")
run(true)
} In Kotlin: compareBy with three selectors sorts the Triples, and firstOrNull with ?: break ends the loop when no held post may be shown.
Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
Dart 3.13 · main.dart
// Post p is written on replica p, with a vector clock: the posts of each replica its author saw.
const sentMs = [0, 12, 14];
const delayMs = [
[0, 10, 40],
[10, 0, 3],
[16, 6, 0],
]; // from the row's replica to the column's
const clocks = [
[1, 0, 0],
[1, 1, 0],
[0, 0, 1],
]; // Ben had seen Asha's post, Chen neither
const texts = ['Asha: Lunch at 1?', 'Ben: Yes!', 'Chen: Late today'];
/// Whether a replica that has shown [seen] may show the post.
bool ready(int post, List<int> seen) =>
[0, 1, 2].every((x) => clocks[post][x] <= seen[x] + (x == post ? 1 : 0));
void run(bool causal) {
final seen = List.generate(
3,
(_) => [0, 0, 0],
); // how many posts of each replica r has shown
final held = List.generate(3, (_) => [false, false, false]);
final shown = List.generate(3, (_) => <String>[]);
final arrivals =
[
for (var p = 0; p < 3; p++)
for (var r = 0; r < 3; r++) (sentMs[p] + delayMs[p][r], p, r),
]..sort(
(a, b) => a.$1 != b.$1
? a.$1 - b.$1
: (a.$2 != b.$2 ? a.$2 - b.$2 : a.$3 - b.$3),
);
for (final (t, p, r) in arrivals) {
held[r][p] = true;
for (var q = 0; q < 3; q++) {
if (held[r][q] && (!causal || ready(q, seen[r]))) {
held[r][q] = false;
shown[r].add(texts[q]);
seen[r][q]++;
q = -1; // look again from the first post
}
}
if (held[r][p]) print(' R${r + 1} at $t ms holds "${texts[p]}"');
}
for (var r = 0; r < 3; r++) {
print(' R${r + 1}: ${shown[r].join(' | ')}');
}
}
void main() {
print('as messages arrive:');
run(false);
print('with causal delivery:');
run(true);
} In Dart: The arrivals are records built by a collection for, and the comparator compares their fields one after another.
Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart
What every language printed
as messages arrive: R1: Asha: Lunch at 1? | Ben: Yes! | Chen: Late today R2: Asha: Lunch at 1? | Ben: Yes! | Chen: Late today R3: Chen: Late today | Ben: Yes! | Asha: Lunch at 1? with causal delivery: R3 at 15 ms holds "Ben: Yes!" R1: Asha: Lunch at 1? | Ben: Yes! | Chen: Late today R2: Asha: Lunch at 1? | Ben: Yes! | Chen: Late today R3: Chen: Late today | Asha: Lunch at 1? | Ben: Yes!
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 12 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 154 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 5 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 9 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 25 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 725 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 186 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 15 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
When messages are shown as they arrive, R3 displays Ben’s reply before Asha’s question, because the link from R2 is fast and the link from R1 is slow. With causal delivery, R3 holds the reply for 25 ms until the question arrives. Step through the second run:
The drawing shows the whole scene; its 9 steps are listed below.
Every step as text
- Asha asks, 0 ms. Asha posts "Lunch at 1?" on R1. Its vector clock is [1, 0, 0], the first message of R1, and R1 shows it at once.
- R2 gets it, 10 ms. The question reaches R2 after 10 ms. Nothing it depends on is missing, so R2 shows it at once.
- Ben replies, 12 ms. Ben posts "Yes!" on R2. His clock is [1, 1, 0], because he had seen message 1 of R1 when he wrote it.
- Chen posts, 14 ms. Chen posts "Late today" on R3, without having seen either message, so its clock is [0, 0, 1].
- The reply arrives, 15 ms. The link from R2 to R3 is fast, 3 ms, so Ben's reply reaches R3 first. Shown as it arrives, it would appear before the question it answers.
- R3 holds it. The reply's clock says its author had seen message 1 of R1, and R3 has shown no message of R1. Causal delivery holds the reply back.
- 20 to 30 ms. Chen's post reaches R2 at 20 ms and R1 at 30 ms, and Ben's reply reaches R1 at 22 ms. None of them waits for anything missing, so each is shown at once.
- The question, 40 ms. Asha's question reaches R3 over the slow 40 ms link. R3 shows it, and now the held reply may follow, 25 ms after it arrived.
- Order, where it matters. Every replica shows the question before the reply. R1 shows Chen's post last and R3 shows it first, which causal consistency allows, because Chen wrote it without seeing the others.
Every replica now shows the question before the reply. The replicas still disagree about Chen’s message: R1 shows it last and R3 shows it first. Causal consistency allows that, because Chen wrote it without seeing the other two. For the same reason, history 6 of the checker, which neither stronger model allows, is legal under causal consistency: the two writes were made without knowledge of each other, so C and D may see them in either order.
The costs are metadata and memory: every message carries its dependencies, and a replica must buffer messages whose causes are missing. Causal consistency is available during a partition only to clients that stay with the replica they have been using (Jepsen calls this sticky available), because a client that moves to a replica that has not seen its earlier operations could see an effect without its cause.
Checkpoint
Check: Causal consistency and causal delivery
Eventual: the copies converge, in time
eventual consistency: The promise that once updates to an object stop, every read sooner or later returns its latest value. It sets no bound on how long that takes and none on what reads return in the meantime. is the weakest common promise: once updates to an object stop, every read sooner or later returns its latest value (Vogels). It says nothing about how long “eventually” is, nothing about what reads return in the meantime and nothing about order, so a client may see a new value and then an old one, or miss its own write. When two replicas accept conflicting writes, the store also needs a rule to merge them, such as keeping the write with the higher timestamp or adding up counter increments. Follow a like counter on two replicas:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- Both copies agree. Both replicas hold 1,000 likes. Each answers reads and accepts likes on its own, without asking the other.
- Three likes on R1. Three users like the post on R1, which counts them at once. R2 has not heard of them yet.
- Two likes on R2. Two other users like it on R2 in the meantime. Each copy is now ahead of the other in a different way.
- A read on R1. One reader's request reaches R1 and shows 1,003.
- A refresh on R2. The refresh reaches R2 and shows 1,002. The count went backwards between two reads of the same reader, and eventual consistency allows it.
- R1 sends its likes. The replicas exchange what they accepted. R1 sends its 3 increments, and R2 adds them.
- R2 sends its likes. R2 sends its 2 increments, and R1 adds them. Adding increments loses none; keeping the copy with the later timestamp would have kept one side's count and dropped the other side's likes.
- Converged. The likes have stopped, and every read on either replica returns 1,005. That is all eventual consistency promises.
Between the likes and the exchange, a reader who asked R1 and then R2 saw 1,003 and then 1,002: the count went backwards, and eventual consistency allows it. Adding up the increments brought both copies to 1,005; keeping the copy with the later timestamp would have kept one side’s count and lost the other side’s likes.
In return, any replica can answer any request at once, including during a partition. That suits data where a short delay harms nobody and conflicting updates merge cleanly: view and like counts, recommendations, presence indicators, the number of items left shown on a listing page (never the one checked at checkout).
Checkpoint
Check: Eventual consistency
Session guarantees: what a single client should never see
The anomalies a user can notice without comparing screens with anyone else are the ones inside their own session: their own change missing, or the page going backwards, as the like count did above. Four session guarantee: A promise about the reads of one session against lagging replicas, such as read-your-writes, rather than about every client at once. It costs far less than keeping every replica current. More in “Replication lag and read-your-writes guarantees” each rule out one such contradiction, and they are cheap enough to add on top of eventual consistency:
| Guarantee | It promises | Without it |
|---|---|---|
| Read your writes | After a client writes, its own later reads see that write | A user saves a profile, refreshes and sees the old version |
| Monotonic reads | A later read never shows a state older than an earlier read showed | A post appears in a feed, then disappears on refresh |
| Monotonic writes | A client’s writes take effect in the order it made them | A rename and then an edit, and some replica applies the edit to the old name |
| Writes follow reads | A write made after reading something takes effect after what it read | A reply stored before the message it answers |
Each guarantee covers one client only and costs little: routing a client to the same replica, or having the client carry the position of the last write it made or saw, so that a replica that is behind can wait or pass the request on. During a partition, read your writes can be kept only for clients that stick to one replica, while monotonic reads, monotonic writes and writes follow reads can be served by any replica (Jepsen’s map).
Name the contradiction before the fix
When a user reports stale data, first say which contradiction they saw: “my own change is missing” calls for read your writes, “the page went backwards” for monotonic reads. Both are fixed per session, never by making every read linearisable.
Checkpoint
Check: The four session guarantees
Stronger models allow fewer histories
The models form a hierarchy: a stronger model allows fewer histories, and every history it allows, the weaker models below it allow too. Of the checker’s six histories, linearisability allowed two and sequential consistency four; causal consistency also allows history 6, and history 5 breaks even read your writes, which causal consistency includes. Jepsen’s map of consistency models also groups the models by how available each can be when the network splits, which is the trade-off the next lesson states precisely.
Consistency models, strongest at the top, grouped by availability during a partition
Text description of the diagram
The diagram shows the consistency models of a single object, such as one key or one register, from the strongest at the top to the weakest at the bottom. An arrow from one model to another means that the first is stronger: every history the first allows, the second allows too.
- The top group is labelled "Cannot serve the minority side of a partition". It holds linearisable (one order of all operations that matches real time) and, below it, sequential (one order in which each process keeps its own order).
- The middle group is labelled "Available if each client sticks to one replica". It holds causal (cause before effect everywhere), then PRAM (each writer's order kept), then read your writes.
- The bottom group is labelled "Available on every replica". It holds one box with monotonic reads and monotonic writes, a box with writes follow reads beside it, and below both, joined to them by dashed arrows, one labelled "weaker still", eventual consistency, which only promises that replicas converge once writes stop.
Sequential points to causal and causal to PRAM. PRAM points to read your writes and to the box of monotonic reads and monotonic writes, and causal also points to writes follow reads, so causal consistency implies all four session guarantees. Read your writes, monotonic reads, monotonic writes and writes follow reads are separate guarantees: none of them implies another.
Read it from the top. Linearisable and sequential consistency need one agreed order, so a replica cut off from the majority must refuse or wait. Causal consistency, Pipelined RAM: every client sees each writer's writes in the order that writer made them, though different clients may interleave the writes of different writers differently. (every writer’s own order kept) and read your writes can keep going during a partition if each client sticks to its replica. Monotonic reads, monotonic writes and writes follow reads survive any partition. Eventual consistency is not on Jepsen’s map; it is drawn at the bottom because it is weaker still, promising only convergence.
Checkpoint
Check: The hierarchy of models
Choose per operation, not per database
A product is not one consistency requirement but many, and one store can offer several models. etcd, for example, serves reads as linearisable by default and offers serializable reads, answered from one member’s local copy, for better performance and availability at the risk of a stale result (etcd API reference). The useful question is therefore, operation by operation: what is the weakest model under which this operation still cannot do harm? Answer the questions for one operation of a product you know:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Model | What reads may return | During a partition | What it costs |
|---|---|---|---|
| Linearisable Typical operations: money, stock, unique names, locks | Good the latest write: no stale read, no flicker back | Poor the minority side refuses or waits | Poor a leader check or a majority on every read and write |
| Causal Typical operations: messages, comments, collaborative edits | Mixed stale values, but never an effect before its cause | Mixed available to clients that stay with their replica | Mixed dependency metadata and buffering |
| Read your writes Typical operations: any screen that shows the user's own changes | Mixed other clients' writes may be stale, never your own | Mixed kept only for clients that stick to one replica | Good routing or a position token per client |
| Monotonic reads Typical operations: timelines, feeds, search results | Mixed stale, but never older than what you read before | Good any replica can serve it | Good routing or a position token per client |
| Eventual Typical operations: counters, views, recommendations | Poor anything, until the copies converge | Good every replica answers at once | Good nothing beyond replication |
How the answers decide
- “Could two clients that act on an old value do harm, such as two debits that both pass the same balance check?” Yes: money, stock or a unique name: rules out Causal, Read your writes, Monotonic reads and Eventual. Two debits can both read the old balance and both succeed. Only a linearisable conditional update, or a transaction, stops the second one.
- “Must an effect never appear before its cause, such as a reply before the message it answers?” Yes: rules out Read your writes, Monotonic reads and Eventual. A reply could appear with nothing to reply to. Causal consistency, or a stronger model, keeps every effect after its cause.
- “Must the user see their own change on the next screen they load?” Yes: rules out Monotonic reads and Eventual. The user would think the save failed and repeat it. Read your writes is needed, or a model that includes it, such as causal.
- “Must a refresh never show an older state than the one before it?” Yes: rules out Read your writes and Eventual. Posts would vanish and come back between refreshes. Monotonic reads is needed, or a model that includes it, such as causal.
- “Must the operation keep working on both sides of a network partition?” Yes, on both sides: rules out Linearisable and adds a caveat to Causal and Read your writes. A linearisable store refuses or waits on the minority side. Causal consistency and read your writes keep working only for clients that stay with their replica.
- “Could two clients that act on an old value do harm, such as two debits that both pass the same balance check?” No and “Must an effect never appear before its cause, such as a reply before the message it answers?” No and “Must the user see their own change on the next screen they load?” No and “Must a refresh never show an older state than the one before it?” No: adds a caveat to Linearisable and Causal. Nothing here needs more than convergence. A stronger model would only add waiting, refusals and metadata.
Linearisability is a property of single operations. A check followed by a separate write is two operations, and two clients can both pass the check before either writes, so money and stock need the check and the write in one conditional update (compare-and-set: A conditional write that stores a new value only if the current value is still the one the client read, as one atomic step, so two clients cannot both pass the same check.) or in a transaction.
A linearisable read, then a separate write
Two clients read a balance of 500 with linearisable reads, and each decides that a debit of 400 is covered. If both write the new balance, 100, one debit vanishes; if both subtract 400, the balance ends at −300. Each read was right; the pair was not atomic. Debit 400 only if the balance is still 500, and let the client that loses retry.
Two habits keep this honest. First, write down what the user would see if the operation used a weaker model, as the table’s reasons do: if nobody would notice or care, the weaker model wins. Second, count the operations that really need linearisability; it is usually a short list (money, stock, uniqueness, locks), and keeping it short is what lets the rest of the system stay fast and available.
Checkpoint
Check: The weakest model per operation
Practise: check a linearisation
The checker above searches for an order. The exercise is the step it repeats: given a proposed order, decide whether it is a linearisation of the history. Its sample tests run in your browser:
Exercise · Medium · Python
Check whether an order linearises a history
Write is_linearisation(history, order) in linearise.py.
history is a list of operations on one register whose initial value is 0. Each operation is a tuple (process, kind, value, start, end): kind is "w" for a write of value or "r" for a read that returned value, and the operation was invoked at start and returned at end (milliseconds). order is a list of indexes into history, a proposed single order of all the operations.
Return True only if all three hold:
ordernames every operation exactly once.- Real time is respected: whenever operation
ireturned before operationjwas invoked (endofiis less thanstartofj),icomes beforejinorder. - Every read returns the value of the latest write before it in
order, or 0 when no write comes before it.
For example, with [("A", "w", 1, 0, 6), ("B", "r", 0, 1, 2), ("C", "r", 1, 3, 5)], the order [1, 0, 2] is a linearisation (B reads before the write takes effect, C after it), and [0, 1, 2] is not, because B's read of 0 would come after the write of 1.
Starter code · linearise.py
def is_linearisation(history, order):
"""True if order (indexes into history) respects real time and the register's values."""
# Replace this line with your code.
return False The sample tests · test_linearise.py
from linearise import is_linearisation
SLOW_WRITE = [("A", "w", 1, 0, 6), ("B", "r", 0, 1, 2), ("C", "r", 1, 3, 5)]
def test_accepts_a_linearisation():
"""accepts an order that respects real time and the register"""
assert is_linearisation(SLOW_WRITE, [1, 0, 2]) is True
def test_read_sees_the_latest_write():
"""rejects an order in which a read misses the latest write before it"""
assert is_linearisation(SLOW_WRITE, [0, 1, 2]) is False
def test_real_time_order():
"""rejects an order that puts an operation before one that returned before it was invoked"""
stale = [("A", "w", 1, 0, 2), ("B", "r", 0, 3, 4)]
assert is_linearisation(stale, [1, 0]) is False
def test_every_operation_once():
"""rejects an order that leaves out or repeats an operation"""
fresh = [("A", "w", 1, 0, 2), ("B", "r", 1, 3, 4)]
assert is_linearisation(fresh, [0, 1]) is True
assert is_linearisation(fresh, [0]) is False
assert is_linearisation(fresh, [0, 1, 1]) is False A hint
Check the three rules one at a time. sorted(order) == list(range(len(history))) settles the first. For the second, build a dictionary from each index to its position in order, then compare every pair of operations. For the third, walk through order with a variable that starts at 0 and changes at every write.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then check yourself on the whole lesson, one question at a time, the idea you know least first:
Practice that adapts to you
Practise this lesson
17 questions about 8 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Design practice: the weakest model for each operation
These are the decisions a design round asks for. Decide each brief first, then compare your choice with the table above:
Checkpoint
Your first decision
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A user renames their profile and, after a refresh, still sees the old name for a few seconds. Other users seeing the change late is acceptable. Which guarantee is missing, and how do you provide it without making every read linearisable?
-
Senior to staff
Your team wants every read in a chat app to be linearisable, to be safe. For message threads, unread counters, typing indicators and account deletion, which consistency model does each actually need, and what would making all of them linearisable cost?
Interview questions
Warm-up (fresher to mid-level): What is the difference between linearisable and sequentially consistent?
Both promise that all clients see one order of all operations, with each client’s own operations in the order it issued them. Linearisability adds real time: if one operation returned before another was invoked, it comes first, so a read that starts after a write has returned must see that write or a newer one. Sequential consistency lets the agreed order lag behind the clock, so a client may read a value that is stale by wall-clock time, as long as every client agrees on the same order. Neither can be kept on the minority side of a network partition, which is why systems that must stay available use weaker models.
Key takeaways
- A consistency model defines which histories of reads and writes are legal; it is the contract that tells you whether a stale read is a bug.
- Linearisable means one order that matches real time, so no stale reads and no flicker back to old values; it needs a leader or majorities, and it errors or waits during a partition.
- Sequential consistency keeps one order that may lag real time, causal consistency keeps only cause before effect, and eventual consistency promises only convergence once writes stop.
- The session guarantees (read your writes, monotonic reads, monotonic writes, writes follow reads) stop one client from contradicting itself, and they are cheap to add.
- Choose the weakest model per operation, and keep the list of linearisable operations short.
Terms in this lesson
- causal consistency
- A consistency model in which causally related operations appear in the same order everywhere, while operations made without knowledge of each other may appear in different orders on different replicas.
- compare-and-set
- A conditional write that stores a new value only if the current value is still the one the client read, as one atomic step, so two clients cannot both pass the same check.
- consistency model
- The contract between a replicated store and its clients that says which values a read may return, given the writes that have happened. It decides whether a stale read is a bug or allowed.
- eventual consistency
- The promise that once updates to an object stop, every read sooner or later returns its latest value. It sets no bound on how long that takes and none on what reads return in the meantime.
- history (of operations)
- The list of every client's operations on an object, each with the time it was invoked, the time it returned and what it wrote or read. Consistency models are rules about which histories are legal.
- linearisability
- The strongest consistency model for one object: every operation appears to take effect at one instant between its invocation and its response, so all clients agree on one order that matches real time.
- sequential consistency
- A consistency model in which all operations appear in one order that every client agrees on and that keeps each client's own operations in the order it issued them, though that order may lag behind real time.
References
- Linearizability: A Correctness Condition for Concurrent Objects (ACM Transactions on Programming Languages and Systems (Herlihy and Wing))
- How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs (IEEE Transactions on Computers (Leslie Lamport))
- Consistency models (Jepsen) (Jepsen)
- Linearizability (Jepsen) (Jepsen)
- Sequential consistency (Jepsen) (Jepsen)
- Causal consistency (Jepsen) (Jepsen)
- Read your writes (Jepsen) (Jepsen)
- Monotonic reads (Jepsen) (Jepsen)
- Monotonic writes (Jepsen) (Jepsen)
- Writes follow reads (Jepsen) (Jepsen)
- Eventually Consistent (All Things Distributed (Werner Vogels))
- etcd API reference: key-value API (linearizable and serializable reads) (etcd (Cloud Native Computing Foundation))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress