Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

Interactive · message by message History 2, as the two clients see it Step through one write and one read of the same register. Each arrow is an invocation or a response, with its time.
History 2, as the two clients see it, step 5 of 5. Messages so far: Client A to The store: write 1, at 0 ms; The store to Client A: ok, at 2 ms; Client B to The store: read, at 3 ms; The store to Client B: 0, at 4 ms.Client AThe storeA.w1 returned at 2 msClient Bwrite 1, at 0 msok, at 2 msread, at 3 ms0, at 4 ms

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. The write returns. At 2 ms the store answers ok. The write has returned, so it took effect somewhere between 0 and 2 ms.
  3. 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.
  4. 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.
  5. 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

  1. Question 1 of 2 History 2 is A.w1 0-2, B.r0 3-4. Which operation comes first in real time?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A's write, because it returned at 2 ms, before B's read was invoked at 3 ms

    Two operations are ordered in real time when one returned before the other was invoked. A's write returned at 2 ms and B's read was invoked at 3 ms, so the write comes first, whatever the read returned.

  2. Question 2 of 2 History 3 is A.w1 0-6, B.r0 1-2, C.r1 3-5. Which pairs of operations are concurrent? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • A's write and B's read
    • A's write and C's read

    Concurrent means overlapping in time. The slow write overlaps both reads, while B's read ended before C's read began, so those two are ordered.

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:

One program · 14 languages Six histories, checked for linearisable and sequential up to n! orders for n operations

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

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:

Interactive · message by message History 4, where a read flickers back Client A's write of 1 is in progress from 0 to 6 ms. Step through two reads that overlap it, and look for one instant that fits both.
History 4, where a read flickers back, step 6 of 6. Messages so far: Client B to The store: read, at 1 ms; The store to Client B: 1, at 2 ms; Client C to The store: read, at 3 ms; The store to Client C: 0, at 4 ms.Client BThe storetook effect by 2 msClient Cread, at 1 ms1, at 2 msread, at 3 ms0, at 4 ms

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. B reads. B's read runs from 1 to 2 ms, inside the write's interval.
  3. B sees 1. B gets 1. So the write's instant came before B's read ended, at 2 ms at the latest.
  4. 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.
  5. 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.
  6. 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

  1. Question 1 of 2 Change history 3 so that C's read also returns 0: A.w1 0-6, B.r0 1-2, C.r0 3-5. Is it linearisable?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Yes, because the write may take effect just before 6 ms, after both reads

    A write in progress may take effect at any instant until it returns. If A's write takes effect just before 6 ms, both reads come before it and return 0, and the order B.r0, C.r0, A.w1 respects real time, so the history is linearisable.

  2. Question 2 of 2 In history 4, B reads 1 from 1 to 2 ms and C reads 0 from 3 to 4 ms, while A's write of 1 runs from 0 to 6 ms. Why is it not linearisable, although neither read is stale on its own?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The write must take effect by 2 ms for B's read, yet C, which starts later, reads the old value

    B's read of 1 places the write's instant before 2 ms. C's read starts at 3 ms, after B's read returned, so it must come after B and so after the write; it returned 0. No instant for the write fits both reads.

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

  1. Question 1 of 2 History 2 is A.w1 0-2, B.r0 3-4. Which order makes it sequentially consistent?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: B.r0, then A.w1

    Sequential consistency needs one order that keeps each client's own operations in order and in which each read returns the latest write before it. Putting B's read first does both, as the checker printed.

  2. Question 2 of 2 Which of the checker's histories does sequential consistency forbid? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • History 6, where C sees 1 then 2 and D sees 2 then 1
    • History 5, where A writes 1 and then reads 0

    Sequential consistency forbids an order that breaks a client's own sequence (history 5) and a history that no single order explains (history 6). Histories 2 and 4 only break real time between clients, which it allows.

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.

One program · 14 languages A reply that arrives before its question, then causal delivery

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!

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:

Interactive · message by message Causal delivery holds a reply until its question arrives Step through the chat with causal delivery. Each message carries a vector clock, the messages of R1, R2 and R3 its author had seen.
Causal delivery holds a reply until its question arrives, step 9 of 9. Messages so far: R1 to R2: Asha [1, 0, 0]; R2 to R3: Ben [1, 1, 0]; R1 to R3: Asha [1, 0, 0].R1shown: Asha, Ben, ChenR2shown: Asha, Ben, ChenR3shown: Chen, Asha, BenAsha [1, 0, 0]Ben [1, 1, 0]Asha [1, 0, 0]

The drawing shows the whole scene; its 9 steps are listed below.

Every step as text
  1. 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.
  2. R2 gets it, 10 ms. The question reaches R2 after 10 ms. Nothing it depends on is missing, so R2 shows it at once.
  3. 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.
  4. Chen posts, 14 ms. Chen posts "Late today" on R3, without having seen either message, so its clock is [0, 0, 1].
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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

  1. Question 1 of 2 In the chat program, Ben's reply reaches R3 at 15 ms and Asha's question reaches R3 at 40 ms. For how many milliseconds does causal delivery hold the reply back?

    Type a number.

    A hint

    The reply can be shown only once the question it depends on has been shown.

    Show the answer: Question 1 of 2

    Answer: 25 ms

    R3 holds the reply from its arrival at 15 ms until it has shown the question at 40 ms: 40 − 15 = 25 ms, as the program and the stepped figure show.

  2. Question 2 of 2 With causal delivery, R1 shows Asha, Ben, Chen and R3 shows Chen, Asha, Ben. Is that a fault?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No: Chen wrote without seeing the others, so his post may sit anywhere

    Ben's reply is caused by Asha's question, so every replica shows the question first. Chen's post is concurrent with both, so causal consistency lets R1 and R3 place it differently.

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:

Interactive · step by step A like counter on two replicas Step through likes that arrive on two replicas, one reader who asks both, and the exchange that makes the copies converge.
A like counter on two replicas, step 8 of 8. Showing Replica R1 (1,005 likes), Replica R2 (1,005 likes), One reader.replicationReplica R11,005 likesReplica R21,005 likesOne reader

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Both copies agree. Both replicas hold 1,000 likes. Each answers reads and accepts likes on its own, without asking the other.
  2. Three likes on R1. Three users like the post on R1, which counts them at once. R2 has not heard of them yet.
  3. 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.
  4. A read on R1. One reader's request reaches R1 and shows 1,003.
  5. 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.
  6. R1 sends its likes. The replicas exchange what they accepted. R1 sends its 3 increments, and R2 adds them.
  7. 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.
  8. 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

  1. One question In the like counter, R1 counts 3 likes and R2 counts 2 before they exchange anything. If the merge kept only the copy written last, R2's 1,002, how many likes would be lost?

    Type a number.

    A hint

    Keeping one copy whole drops everything the other side counted.

    Show the answer: One question

    Answer: 3 likes

    Keeping R2's 1,002 drops the 3 likes that only R1 counted, so the counter shows 1,002 instead of 1,005. Adding each side's increments keeps all five.

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

  1. Question 1 of 2 A post appears in a user's feed, disappears when they refresh, then comes back later. Which guarantee is missing?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Monotonic reads

    Two reads of one user went backwards, from a state with the post to an older one without it. Monotonic reads rules exactly that out, for example by routing the user to one replica or by carrying the position of the last state they saw.

  2. Question 2 of 2 During a partition, which of these session guarantees can be kept only for clients that stick to one replica?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Read your writes

    On Jepsen's map, read your writes is among the models that stay available only for clients that stick to their replica. Monotonic reads, monotonic writes and writes follow reads stay available on every replica.

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 from linearisable down to eventual, in three groups by how available each can be during a network partition.Cannot serve the minority side of a partitionAvailable if each client sticks to one replicaAvailable on every replicaLinearisableone order, matches real timeSequentialone order, each process in its own orderCausalcause before effect everywherePRAMeach writer's order keptRead your writesMonotonic readsMonotonic writesWrites follow readsEventualreplicas converge once writes stopweaker still

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.

  1. 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).
  2. 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.
  3. 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

  1. One question During a network partition, which of these models cannot be kept on the side that holds only a minority of the replicas? Choose every one.

    Choose every answer that is right.

    Show the answer: One question

    Answer:

    • Sequential
    • Linearisable

    The two models that need a single agreed order of all operations cannot be extended by a minority cut off from the rest. The weaker models on the map stay available.

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:

Interactive · answer and compare The weakest model for one operation Answer five questions about one operation of a product you know. The table rules out the models that would let it do harm, and says why.
Could two clients that act on an old value do harm, such as two debits that both pass the same balance check?
Must an effect never appear before its cause, such as a reply before the message it answers?
Must the user see their own change on the next screen they load?
Must a refresh never show an older state than the one before it?
Must the operation keep working on both sides of a network partition?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Five consistency models against what reads may return, what a partition does and what each costs
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

  1. Question 1 of 2 An operation debits an account only if the balance covers the debit. What does it need?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A conditional update: debit only if the balance is still what was read

    Linearisability is a property of single operations. Only a conditional update (compare-and-set) or a transaction makes the check and the write one step.

  2. Question 2 of 2 A news feed may lag a few seconds behind, but a refresh must never lose posts already shown. What is the weakest model that keeps it correct?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Monotonic reads

    The requirement is about one reader's reads never going backwards, which is monotonic reads, served with routing or a position token per client.

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:

  1. order names every operation exactly once.
  2. Real time is respected: whenever operation i returned before operation j was invoked (end of i is less than start of j), i comes before j in order.
  3. 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.

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.

  1. Question 1 of 17 A consistency model is defined over a history. What does a history record about each operation?

    Choose one answer.

    Show the answer: Question 1 of 17

    Answer: Its client, what it wrote or read, when it was invoked and when it returned

    A history lists every client's operations with their invocation and response times and their values. A consistency model says which such histories are legal.

  2. Question 2 of 17 A.w1 0-4, B.r1 1-2, C.r1 5-6. Which statement is right?

    Choose one answer.

    Show the answer: Question 2 of 17

    Answer: B's read overlaps the write, and C's read comes after both in real time

    Two operations are concurrent when their intervals overlap and ordered when one returned before the other was invoked. B overlaps the write; C comes after both.

  3. Question 3 of 17 In a linearisable store, a write of 1 returned at 2 ms. A read on another client starts at 5 ms. What may it return?

    Choose one answer.

    Show the answer: Question 3 of 17

    Answer: 1, or a value written later

    Once a write has returned, every operation that starts afterwards comes later in the single order, so a read sees 1 or a newer value.

  4. Question 4 of 17 A leader holds the value 1 and answers reads from its own copy without checking that it still leads. A partition cuts it off, and a new leader on the other side accepts a write of 2 that returns. A client then reads from the old leader. What can it see?

    Choose one answer.

    Show the answer: Question 4 of 17

    Answer: The old value, 1, which breaks linearisability

    A replica cannot know alone that no newer write has taken effect elsewhere. A leader that answers without confirming it still leads can return a stale value after a write has returned, so linearisable reads need that check or a majority.

  5. Question 5 of 17 A write of 1 returns at 2 ms. A read on another client, invoked at 3 ms, returns the old value, 0. Which models does this history break?

    Choose one answer.

    Show the answer: Question 5 of 17

    Answer: Linearisability only; it is still sequentially consistent

    Linearisability orders operations by real time across clients, so a read invoked after the write returned must see 1. Sequential consistency only keeps each client's own order, so the order "read 0, then write 1" is legal. This is history 2 of the checker.

  6. Question 6 of 17 A client saves its new address, gets ok, then reads its own address and gets the old one. Which of linearisable, sequential and causal consistency allow that?

    Choose one answer.

    Show the answer: Question 6 of 17

    Answer: None of them

    This is history 5 of the checker. Breaking a client's own order is forbidden by sequential consistency, and causal consistency, which includes read your writes, forbids it too.

  7. Question 7 of 17 In a group chat, a reply must never appear before the message it answers, but two unrelated messages may appear in either order. What is the weakest model that guarantees this?

    Choose one answer.

    Show the answer: Question 7 of 17

    Answer: Causal consistency

    Causal consistency keeps every effect after its cause and lets causally unrelated messages appear in different orders, as the chat program shows on R1 and R3.

  8. Question 8 of 17 What does causal delivery do with a message that arrives before a message it depends on?

    Choose one answer.

    Show the answer: Question 8 of 17

    Answer: It holds the message back until the replica has shown everything it depends on

    Each message carries a vector clock, the messages of each replica its author had seen. A replica holds a message until it has shown all of them; that is why R3 held Ben's reply for 25 ms.

  9. Question 9 of 17 History 6: A writes 1 and B writes 2 at the same moment; C reads 1 then 2, while D reads 2 then 1. Which models allow it?

    Choose one answer.

    Show the answer: Question 9 of 17

    Answer: Causal consistency, but neither sequential consistency nor linearisability

    No single order explains both readers, so the two models with one agreed order forbid history 6. The writes are concurrent, so causal consistency lets C and D see them in different orders.

  10. Question 10 of 17 What does eventual consistency promise on its own?

    Choose one answer.

    Show the answer: Question 10 of 17

    Answer: If writes stop, every replica ends up returning the same, latest value

    Eventual consistency only promises convergence once updates stop. It sets no bound on staleness, and it lets a client see an older value after a newer one or miss its own write.

  11. Question 11 of 17 A shop shows the number of items left on its listing page and checks the stock again at checkout. Where may it use eventual consistency?

    Choose one answer.

    Show the answer: Question 11 of 17

    Answer: On the listing page only

    Eventual consistency suits data where a short delay harms nobody, like the number shown on a listing. The count checked at checkout decides a sale, so it needs the check and the write as one linearisable operation.

  12. Question 12 of 17 A user posts a comment, refreshes the page and expects to see it, while other users may see it a few seconds later. Which guarantee does this need?

    Choose one answer.

    Show the answer: Question 12 of 17

    Answer: Read your writes

    The requirement is about one user's own write and their own later read, which is exactly read your writes. Linearisability would also satisfy it, at a much higher price, and eventual consistency alone allows the refresh to show the old page.

  13. Question 13 of 17 A user renames a file and then edits it, and one replica applies the edit to the old name. Which guarantee is missing?

    Choose one answer.

    Show the answer: Question 13 of 17

    Answer: Monotonic writes

    Monotonic writes makes a client's writes take effect in the order it made them, so the edit lands on the renamed file.

  14. Question 14 of 17 Model X is stronger than model Y. What follows?

    Choose one answer.

    Show the answer: Question 14 of 17

    Answer: Every history that X allows, Y allows too

    The models form a hierarchy: linearisability allowed two of the checker's six histories, sequential consistency four, and causal consistency five.

  15. Question 15 of 17 A causally consistent store loses contact between two data centres. A client that has been using one of them switches to the other. What can go wrong?

    Choose one answer.

    Show the answer: Question 15 of 17

    Answer: It may see an effect without its cause, because that side has not seen the client's earlier operations

    On Jepsen's map causal consistency is sticky available. A client that moves to a replica that has not seen its earlier operations could see an effect before its cause.

  16. Question 16 of 17 What is the weakest model that keeps a post's like count correct enough?

    Choose one answer.

    Show the answer: Question 16 of 17

    Answer: Eventual consistency

    A like count can lag and still merge correctly, so eventual consistency fits, with a merge that adds the increments of each side.

  17. Question 17 of 17 Two clients read a seat as free with linearisable reads, and then each writes its own booking with a separate write. What goes wrong, and what fixes it?

    Choose one answer.

    Show the answer: Question 17 of 17

    Answer: Both can book the seat; one conditional write or a transaction fixes it

    A check followed by a separate write is two operations. Booking only if the seat is still free, in one compare-and-set or one transaction, lets exactly one client win.

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

  1. Question 1 of 2 Design brief: a user renames their profile and still sees the old name for a few seconds after a refresh, while other users may see the change late. What is your first decision?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Give that user's session read your writes, by routing or a position token

    Name the contradiction first: the user's own write is missing from the user's own read, which is read your writes. It costs little because it covers one session: send that user's reads to a copy that has the write.

  2. Question 2 of 2 Design brief: a team wants every read in a chat app to be linearisable, to be safe. What do the unread counters need?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Eventual consistency: a count a few seconds behind harms nobody

    Write down what a user would see with a weaker model: an unread count two seconds behind, which harms nobody. Keep the strong models for the operations where a wrong answer hurts.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.