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 8 – Replication

Replication lag and read-your-writes guarantees

Why reads from lagging followers show users old data, the three anomalies it causes, and how sessions get read-your-writes and monotonic reads.

  • 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 core, Pro depth: 8 of 25 minutes free
Free: Three anomalies come from one cause: reads at different positions · Four routing rules, and the guarantees each gives · A position token gives all three guarantees, in a few lines · Practise: route reads by token. With a pass: 2 in-depth parts and 3 interview answers, about 17 minutes.

What you will learn

  • Recognise the three anomalies that replica lag causes, from what a user sees
  • Compare four read-routing rules by the session guarantees each gives
  • Implement a position-token router that gives read-your-writes, monotonic reads and consistent prefix reads
  • Measure lag in time and in log position, and size a read-from-leader window from it

Before you start

What you’ll learn · Before you start

What you will learn

  • Recognise the three anomalies that replica lag causes, from what a user sees
  • Compare four read-routing rules by the session guarantees each gives
  • Implement a position-token router that gives read-your-writes, monotonic reads and consistent prefix reads
  • Measure lag in time and in log position, and size a read-from-leader window from it

Before you start

On this page

Asha renames herself, opens her profile a second later and sees her old name: the read landed on a follower three seconds behind the leader. replication lag: How far a follower is behind its leader: the log positions it has not applied yet, or the time before a commit on the leader becomes visible on the follower. More in “Leader-follower replication and replication logs” is the time between a write committing on the leader and the same write becoming visible on a follower. It is usually milliseconds, and it grows under heavy load, behind long-running queries and after network trouble. Every read sent to a follower may return data from before the lag, and users notice in three ways: their own change is missing, a refresh shows older data than the page before, or an answer appears without its question.

read-your-writes: A session guarantee: after a session writes, its later reads show that write, even when they are served by followers that lag behind the leader. guarantees and two companions, monotonic reads: A session guarantee: once a session has seen some state of the data, its later reads never show an older one, so a refresh never goes back in time. and consistent prefix reads: A session guarantee: a session sees writes in the order they were made, so it never sees an answer without the question it answers., rule these out without sending every read to the leader. One routing rule, a position token, gives all three, and it fits in a few lines of code.

Three anomalies come from one cause: reads at different positions

A follower shows the leader’s data as it was a moment ago. That alone is harmless. The trouble starts when one person’s reads land on followers at different positions, or on a follower that has not reached that person’s own write.

A leader at position 8 streams to follower A, at 7, and follower B, at 5; reads that land on B show old or incomplete data.Leader: positions 1 to 86 a question, 7 its answer,8 Asha's new nameFollower Aapplied up to 7Follower Bapplied up to 5Reads that land on both followers 1. Asha renamed herself and reads from B:her old name is back 2. Ravi reads from A, then from B:the answer disappears 3. A page reads 7 from A and 6 from B:an answer to a missing questionstreamed3 s latereadsreads

One lagging follower, three anomalies

Text description of the diagram

At the top is the leader, whose log holds positions 1 to 8. Position 6 is a question someone posted, position 7 is its answer, and position 8 is Asha's change of name.

The leader streams its log to two followers. Follower A has applied up to position 7, one behind the leader. Follower B gets the log three seconds late and has applied only up to position 5.

Both followers serve reads, and the box at the bottom lists three readers whose reads land on both: 1. Asha, who made the change at position 8, reads her profile from follower B and sees her old name: her own write is missing. 2. Ravi reads the thread from follower A, where the answer is present, then refreshes and lands on follower B, where it is not: what he sees goes back in time. 3. A page loads the answer (position 7) from follower A and the question (position 6) from follower B, so it shows an answer to a question that is not there.

Step through the diagram’s three readers, one read at a time:

Interactive · step by step Three readers, two followers, one cause Step through the three readers of the diagram above. Watch which follower each read lands on, and the position it has applied.
Three readers, two followers, one cause, step 6 of 6. Showing Leader, Follower A, Follower B, Readers. In focus: Follower A, Follower B.log3 s latereadsreadsLeaderpositions 1 to 8Follower Aapplied up to 7Follower Bapplied up to 5Readersone session each

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

Every step as text
  1. One log, two positions. The leader has written positions 1 to 8. Position 6 is a question, 7 its answer and 8 Asha's new name. Follower A has applied up to 7, follower B, three seconds behind, up to 5.
  2. Asha reads from B. Asha renamed herself at position 8, then opens her profile. The read lands on B, which stopped at 5, and her old name is back. Her own write is missing.
  3. Ravi reads from A. Ravi opens the thread. His read lands on A, which has applied position 7, and he sees the answer.
  4. Ravi refreshes on B. His refresh lands on B, at 5, and the answer he has just read is gone. What he sees went back in time.
  5. A page reads both. A page loads the answer, position 7, from A and the question it answers, position 6, from B. B has not reached 6, so the page shows an answer to a question that is not there.
  6. One cause. Each follower shows a correct earlier state. The trouble is that one session's reads landed at different positions, or at a position before the session's own write.

Each anomaly as a user sees it, and the guarantee that rules it out:

Anomaly What the user sees The guarantee that rules it out
A missing write “I saved it, and it is gone” Read-your-writes: a session sees its own writes
Time running backwards “The comment vanished when I refreshed” Monotonic reads: later reads never show an older state
Effect before cause “An answer to a question that is not there” Consistent prefix reads: writes appear in the order they were made

The names come from research on replicated data; Doug Terry’s report that explains them through the score of a baseball game is the friendliest introduction, and calls the first one “read my writes” (Terry). All three concern the reads of one session, not the whole system, which is why they are called 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. and why they cost far less than keeping every follower current. Read-your-writes is sticky available: it survives a network partition as long as each client keeps talking to the same server (Jepsen).

Checkpoint

Check: The three anomalies and their cause

  1. Question 1 of 2 Meera saves a new delivery address. The checkout page reads her addresses from a follower that is two seconds behind and shows none. Which guarantee is missing?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Read-your-writes

    Read-your-writes promises that a session sees its own writes. A follower two seconds behind has not applied her new address yet, so a read there shows the past to the one person who knows it changed.

  2. Question 2 of 2 A support agent opens a ticket and sees the reply "Refund issued", but not the customer's message that the reply answers. Which guarantee is missing?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Consistent prefix reads

    Any single follower shows a prefix of the leader's log, so reading both messages from one follower, or with one token, shows the message before its reply. The anomaly comes from mixing two positions in one page.

Four routing rules, and the guarantees each gives

Replication lag cannot be removed, so every fix decides where a read goes. Four are in common use:

  • Read from the leader after a write. For a short window after a session writes, its reads go to the leader, which always has the write.
  • Sticky follower. Each session reads from one follower, so its reads never go back in time.
  • Position token. The session remembers the log position of its latest write or read, and reads only from followers that have applied it.
  • Lag-aware routing. Followers more than a set limit behind are taken out of rotation for everyone.

Only the position token gives all three guarantees by construction. It needs a little bookkeeping: the leader tells the client each commit’s position, and followers report how far they have applied. Read-your-writes guarantees from a time window are only as good as the window, because a follower further behind than the window is long still lacks the write.

Four ways to route reads
Criterion Leader after a writeSticky followerPosition tokenLag-aware routing
Read-your-writes Inside the window onlyNo, if the follower lags behind the writeYesNo
Monotonic reads No: reads jump from the leader to a followerYesYesNo
Consistent prefix NoYes, for reads on one followerYesNo
Main cost Leader reads after every writeUneven load; the session moves when its follower failsPositions to carry; an occasional waitFewer followers during trouble
When to choose A quick first step for profile editsFeeds and lists that must not jump backAnything a user just changedEvery design, as a safety net beside the others

Key idea

Every fix decides where a read goes; only a position token, which reads from followers that have applied the session’s latest write or read, gives all three guarantees at once.

Checkpoint

Check: Four read-routing rules compared

  1. Question 1 of 2 Which guarantees does a sticky follower give, where each session always reads from the same follower? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • Consistent prefix reads, for reads on that follower
    • Monotonic reads

    A sticky follower removes jumps back in time and orphaned answers, but when its own follower lags, a user still sees their change disappear. Only the position token gives all three by construction.

  2. Question 2 of 2 Reads go to the leader for 1 s after a session writes. When does a session still miss its own write?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: When its next read comes after the window and lands on a follower more than a second behind

    Read-your-writes guarantees from a time window are only as good as the window, because a follower further behind than the window is long still lacks the write.

A position token gives all three guarantees, in a few lines

Each write returns its commit position, a session keeps the highest position it has written or read, and a read goes to a follower that has applied the session’s token, or to the leader when none has. The program runs the diagram’s three readers twice, through a plain balancer that picks the follower in the diagram and through the token router:

One program · 14 languages A position-token router against a plain balancer one scan of the log per read

Python 3.14 · main.py

LOG = [  # positions 2 to 5 change other keys
    (1, "name", "Asha"),
    (6, "question", "Which mode?"),
    (7, "answer", "Use ANY 1"),
    (8, "name", "Asha Rao"),
]
APPLIED = {"A": 7, "B": 5}
CASES = [  # token, keys read, the followers a plain balancer picks
    ("Asha reads her name after renaming at 8", 8, ["name"], "B"),
    ("Ravi reads the answer, then refreshes", 0, ["answer", "answer"], "AB"),
    ("A page shows an answer and its question", 0, ["answer", "question"], "AB"),
]


def seen(key: str, server: str, position: int) -> str:
    values = [value for at, k, value in LOG if at <= position and k == key]
    return f"{key} {values[-1] if values else 'nothing'} from {server}"


def route(token: int) -> tuple[str, int]:
    # The first follower at or past the token serves the read, else the leader.
    leader = ("leader", LOG[-1][0])
    return next(((f, at) for f, at in APPLIED.items() if at >= token), leader)


for title, token, keys, picks in CASES:
    naive, routed = [], []
    for key, pick in zip(keys, picks):
        naive.append(seen(key, pick, APPLIED[pick]))
        server, at = route(token)
        routed.append(seen(key, server, at))
        token = max(token, at)  # the token rises to the position read
    print(title)
    print(f"  naive: {', '.join(naive)}")
    print(f"  token: {', '.join(routed)} (token {token})")

In Python: next() with a default returns the leader when no follower has reached the token, and zip pairs each key with the follower a plain balancer picks.

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

const LOG = [
  // positions 2 to 5 change other keys
  [1, "name", "Asha"],
  [6, "question", "Which mode?"],
  [7, "answer", "Use ANY 1"],
  [8, "name", "Asha Rao"],
];
const APPLIED = { A: 7, B: 5 };
const CASES = [
  // token, keys read, the followers a plain balancer picks
  ["Asha reads her name after renaming at 8", 8, ["name"], "B"],
  ["Ravi reads the answer, then refreshes", 0, ["answer", "answer"], "AB"],
  ["A page shows an answer and its question", 0, ["answer", "question"], "AB"],
];

function seen(key, server, position) {
  const values = LOG.filter(([at, k]) => at <= position && k === key);
  return `${key} ${values.length ? values[values.length - 1][2] : "nothing"} from ${server}`;
}

// The first follower at or past the token serves the read, else the leader.
const route = (token) =>
  Object.entries(APPLIED).find(([, at]) => at >= token) ?? ["leader", LOG[LOG.length - 1][0]];

for (let [title, token, keys, picks] of CASES) {
  const naive = [];
  const routed = [];
  keys.forEach((key, i) => {
    naive.push(seen(key, picks[i], APPLIED[picks[i]]));
    const [server, at] = route(token);
    routed.push(seen(key, server, at));
    token = Math.max(token, at); // the token rises to the position read
  });
  console.log(title);
  console.log(`  naive: ${naive.join(", ")}`);
  console.log(`  token: ${routed.join(", ")} (token ${token})`);
}

In JavaScript: find returns undefined when no follower qualifies, so ?? supplies the leader; Object.entries keeps the followers in the order they were written.

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.Map;

public class Main {
  record Write(int position, String key, String value) {}

  record Case(String title, int token, String picks, String... keys) {}

  // positions 2 to 5 change other keys
  static final List<Write> LOG =
      List.of(
          new Write(1, "name", "Asha"),
          new Write(6, "question", "Which mode?"),
          new Write(7, "answer", "Use ANY 1"),
          new Write(8, "name", "Asha Rao"));
  static final List<String> FOLLOWERS = List.of("A", "B");
  static final Map<String, Integer> APPLIED = Map.of("A", 7, "B", 5);

  static String seen(String key, String server, int position) {
    String value = "nothing";
    for (Write write : LOG) {
      if (write.position() <= position && write.key().equals(key)) {
        value = write.value();
      }
    }
    return key + " " + value + " from " + server;
  }

  // The first follower at or past the token serves the read, else the leader.
  static Map.Entry<String, Integer> route(int token) {
    for (String follower : FOLLOWERS) {
      if (APPLIED.get(follower) >= token) {
        return Map.entry(follower, APPLIED.get(follower));
      }
    }
    return Map.entry("leader", LOG.get(LOG.size() - 1).position());
  }

  public static void main(String[] args) {
    // token, the followers a plain balancer picks, keys read
    List<Case> cases =
        List.of(
            new Case("Asha reads her name after renaming at 8", 8, "B", "name"),
            new Case("Ravi reads the answer, then refreshes", 0, "AB", "answer", "answer"),
            new Case("A page shows an answer and its question", 0, "AB", "answer", "question"));
    for (Case c : cases) {
      int token = c.token();
      List<String> naive = new ArrayList<>();
      List<String> routed = new ArrayList<>();
      for (int i = 0; i < c.keys().length; i++) {
        String key = c.keys()[i];
        String pick = String.valueOf(c.picks().charAt(i));
        naive.add(seen(key, pick, APPLIED.get(pick)));
        var read = route(token);
        routed.add(seen(key, read.getKey(), read.getValue()));
        token = Math.max(token, read.getValue()); // the token rises to the position read
      }
      System.out.println(c.title());
      System.out.println("  naive: " + String.join(", ", naive));
      System.out.println("  token: " + String.join(", ", routed) + " (token " + token + ")");
    }
  }
}

In Java: Case takes its keys as a varargs component, and Map.of keeps no order, so FOLLOWERS fixes the order in which route tries them.

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

#include <algorithm>
#include <map>
#include <print>
#include <string>
#include <utility>
#include <vector>

struct Write {
  int position;
  std::string key, value;
};

struct Case {
  std::string title;
  int token;
  std::vector<std::string> keys;
  std::string picks;
};

// positions 2 to 5 change other keys
const std::vector<Write> log_ = {{1, "name", "Asha"},
                                 {6, "question", "Which mode?"},
                                 {7, "answer", "Use ANY 1"},
                                 {8, "name", "Asha Rao"}};
const std::map<std::string, int> applied = {{"A", 7}, {"B", 5}};

std::string seen(const std::string &key, const std::string &server,
                 int position) {
  std::string value = "nothing";
  for (const Write &w : log_) {
    if (w.position <= position && w.key == key) {
      value = w.value;
    }
  }
  return key + " " + value + " from " + server;
}

// The first follower at or past the token serves the read, else the leader.
std::pair<std::string, int> route(int token) {
  for (const auto &[follower, at] : applied) {
    if (at >= token) {
      return {follower, at};
    }
  }
  return {"leader", log_.back().position};
}

int main() {
  // token, keys read, the followers a plain balancer picks
  const std::vector<Case> cases = {
      {"Asha reads her name after renaming at 8", 8, {"name"}, "B"},
      {"Ravi reads the answer, then refreshes", 0, {"answer", "answer"}, "AB"},
      {"A page shows an answer and its question",
       0,
       {"answer", "question"},
       "AB"}};
  for (const Case &c : cases) {
    int token = c.token;
    std::string naive, routed;
    for (std::size_t i = 0; i < c.keys.size(); i++) {
      const std::string pick(1, c.picks[i]), sep = i ? ", " : "";
      naive += sep + seen(c.keys[i], pick, applied.at(pick));
      const auto [server, at] = route(token);
      routed += sep + seen(c.keys[i], server, at);
      token = std::max(token, at); // the token rises to the position read
    }
    std::println("{}\n  naive: {}\n  token: {} (token {})", c.title, naive,
                 routed, token);
  }
}

In C++: std::map keeps its keys sorted, so route tries A before B, and applied.at(pick) would throw if a pick named no follower.

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

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

// positions 2 to 5 change other keys
(int Position, string Key, string Value)[] log =
[
    (1, "name", "Asha"),
    (6, "question", "Which mode?"),
    (7, "answer", "Use ANY 1"),
    (8, "name", "Asha Rao"),
];
(string Name, int Applied)[] followers = [("A", 7), ("B", 5)];
// token, keys read, the followers a plain balancer picks
(string Title, int Token, string[] Keys, string Picks)[] cases =
[
    ("Asha reads her name after renaming at 8", 8, ["name"], "B"),
    ("Ravi reads the answer, then refreshes", 0, ["answer", "answer"], "AB"),
    ("A page shows an answer and its question", 0, ["answer", "question"], "AB"),
];

foreach (var (title, first, keys, picks) in cases)
{
    int token = first;
    var naive = new List<string>();
    var routed = new List<string>();
    for (int i = 0; i < keys.Length; i++)
    {
        var pick = followers.First(f => f.Name == picks[i].ToString());
        naive.Add(Seen(keys[i], pick.Name, pick.Applied));
        var (server, at) = Route(token);
        routed.Add(Seen(keys[i], server, at));
        token = Math.Max(token, at); // the token rises to the position read
    }
    Console.WriteLine(title);
    Console.WriteLine($"  naive: {string.Join(", ", naive)}");
    Console.WriteLine($"  token: {string.Join(", ", routed)} (token {token})");
}

string Seen(string key, string server, int position)
{
    string? value = log.LastOrDefault(w => w.Position <= position && w.Key == key).Value;
    return $"{key} {value ?? "nothing"} from {server}";
}

// The first follower at or past the token serves the read, else the leader.
(string Name, int Applied) Route(int token) =>
    followers.FirstOrDefault(f => f.Applied >= token, (Name: "leader", Applied: log[^1].Position));

In C#: FirstOrDefault takes the leader as its default, and LastOrDefault returns a default tuple, whose Value is null, when no write matches.

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

#include <stdio.h>
#include <string.h>

typedef struct {
  int position;
  const char *key, *value;
} Write;

// positions 2 to 5 change other keys
static const Write log_[] = {{1, "name", "Asha"},
                             {6, "question", "Which mode?"},
                             {7, "answer", "Use ANY 1"},
                             {8, "name", "Asha Rao"}};
static const struct {
  const char *name;
  int applied;
} followers[] = {{"A", 7}, {"B", 5}};
enum { LOG_SIZE = sizeof log_ / sizeof log_[0], FOLLOWERS = 2 };

static void seen(const char *key, const char *server, int position) {
  const char *value = "nothing";
  for (size_t i = 0; i < LOG_SIZE; i++) {
    if (log_[i].position <= position && strcmp(log_[i].key, key) == 0) {
      value = log_[i].value;
    }
  }
  printf("%s %s from %s", key, value, server);
}

// The first follower at or past the token serves the read, else the leader
// (-1).
static int route(int token) {
  for (int f = 0; f < FOLLOWERS; f++) {
    if (followers[f].applied >= token) {
      return f;
    }
  }
  return -1;
}

int main(void) {
  // token, keys read, the followers a plain balancer picks
  static const struct {
    const char *title;
    int token;
    const char *keys[2];
    const char *picks;
  } cases[] = {
      {"Asha reads her name after renaming at 8", 8, {"name"}, "B"},
      {"Ravi reads the answer, then refreshes", 0, {"answer", "answer"}, "AB"},
      {"A page shows an answer and its question",
       0,
       {"answer", "question"},
       "AB"}};
  for (size_t c = 0; c < sizeof cases / sizeof cases[0]; c++) {
    const size_t reads = strlen(cases[c].picks);
    printf("%s\n  naive: ", cases[c].title);
    for (size_t i = 0; i < reads; i++) {
      const int f = cases[c].picks[i] - 'A';
      printf("%s", i ? ", " : "");
      seen(cases[c].keys[i], followers[f].name, followers[f].applied);
    }
    int token = cases[c].token;
    printf("\n  token: ");
    for (size_t i = 0; i < reads; i++) {
      const int f = route(token);
      const int at = f < 0 ? log_[LOG_SIZE - 1].position : followers[f].applied;
      printf("%s", i ? ", " : "");
      seen(cases[c].keys[i], f < 0 ? "leader" : followers[f].name, at);
      token = at > token ? at : token; // the token rises to the position read
    }
    printf(" (token %d)\n", token);
  }
}

In C: C has no map in its standard library, so a pick's letter minus 'A' indexes the followers, and route returns -1 for the leader.

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);

// positions 2 to 5 change other keys
const LOG = [
    [1, 'name', 'Asha'],
    [6, 'question', 'Which mode?'],
    [7, 'answer', 'Use ANY 1'],
    [8, 'name', 'Asha Rao'],
];
const APPLIED = ['A' => 7, 'B' => 5];
// token, keys read, the followers a plain balancer picks
const CASES = [
    ['Asha reads her name after renaming at 8', 8, ['name'], 'B'],
    ['Ravi reads the answer, then refreshes', 0, ['answer', 'answer'], 'AB'],
    ['A page shows an answer and its question', 0, ['answer', 'question'], 'AB'],
];

function seen(string $key, string $server, int $position): string
{
    $value = 'nothing';
    foreach (LOG as [$at, $k, $v]) {
        if ($at <= $position && $k === $key) {
            $value = $v;
        }
    }
    return "{$key} {$value} from {$server}";
}

/**
 * The first follower at or past the token serves the read, else the leader.
 *
 * @return array{string, int}
 */
function route(int $token): array
{
    foreach (APPLIED as $follower => $at) {
        if ($at >= $token) {
            return [$follower, $at];
        }
    }
    return ['leader', LOG[count(LOG) - 1][0]];
}

foreach (CASES as [$title, $token, $keys, $picks]) {
    $naive = $routed = [];
    foreach ($keys as $i => $key) {
        $naive[] = seen($key, $picks[$i], APPLIED[$picks[$i]]);
        [$server, $at] = route($token);
        $routed[] = seen($key, $server, $at);
        $token = max($token, $at); // the token rises to the position read
    }
    echo "{$title}\n  naive: " . implode(', ', $naive) . "\n";
    echo '  token: ' . implode(', ', $routed) . " (token {$token})\n";
}

In PHP: Arrays keep their insertion order, so foreach tries A before B, and the string offset $picks[$i] gives one letter.

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

// positions 2 to 5 change other keys
const LOG: [(u32, &str, &str); 4] = [
    (1, "name", "Asha"),
    (6, "question", "Which mode?"),
    (7, "answer", "Use ANY 1"),
    (8, "name", "Asha Rao"),
];
const APPLIED: [(&str, u32); 2] = [("A", 7), ("B", 5)];
// token, keys read, the followers a plain balancer picks
const CASES: [(&str, u32, &[&str], &str); 3] = [
    ("Asha reads her name after renaming at 8", 8, &["name"], "B"),
    (
        "Ravi reads the answer, then refreshes",
        0,
        &["answer", "answer"],
        "AB",
    ),
    (
        "A page shows an answer and its question",
        0,
        &["answer", "question"],
        "AB",
    ),
];

fn seen(key: &str, server: &str, position: u32) -> String {
    let found = LOG
        .iter()
        .rev()
        .find(|&&(at, k, _)| at <= position && k == key);
    let value = found.map_or("nothing", |&(_, _, value)| value);
    format!("{key} {value} from {server}")
}

/// The first follower at or past the token serves the read, else the leader.
fn route(token: u32) -> (&'static str, u32) {
    let leader = ("leader", LOG[LOG.len() - 1].0);
    APPLIED
        .into_iter()
        .find(|&(_, at)| at >= token)
        .unwrap_or(leader)
}

fn main() {
    for (title, mut token, keys, picks) in CASES {
        let (mut naive, mut routed) = (Vec::new(), Vec::new());
        for (key, pick) in keys.iter().zip(picks.bytes()) {
            let (name, at) = APPLIED[usize::from(pick - b'A')];
            naive.push(seen(key, name, at));
            let (server, at) = route(token);
            routed.push(seen(key, server, at));
            token = token.max(at); // the token rises to the position read
        }
        println!("{title}\n  naive: {}", naive.join(", "));
        println!("  token: {} (token {token})", routed.join(", "));
    }
}

In Rust: pick - b'A' turns a byte of picks into an index, and unwrap_or supplies the leader when find returns None.

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

const LOG: [number, string, string][] = [
  // positions 2 to 5 change other keys
  [1, "name", "Asha"],
  [6, "question", "Which mode?"],
  [7, "answer", "Use ANY 1"],
  [8, "name", "Asha Rao"],
];
const APPLIED: Record<string, number> = { A: 7, B: 5 };
const CASES: [string, number, string[], string][] = [
  // token, keys read, the followers a plain balancer picks
  ["Asha reads her name after renaming at 8", 8, ["name"], "B"],
  ["Ravi reads the answer, then refreshes", 0, ["answer", "answer"], "AB"],
  ["A page shows an answer and its question", 0, ["answer", "question"], "AB"],
];

function seen(key: string, server: string, position: number): string {
  const values = LOG.filter(([at, k]) => at <= position && k === key);
  return `${key} ${values.length ? values[values.length - 1][2] : "nothing"} from ${server}`;
}

// The first follower at or past the token serves the read, else the leader.
const route = (token: number): [string, number] =>
  Object.entries(APPLIED).find(([, at]) => at >= token) ?? ["leader", LOG[LOG.length - 1][0]];

for (let [title, token, keys, picks] of CASES) {
  const naive: string[] = [];
  const routed: string[] = [];
  keys.forEach((key, i) => {
    naive.push(seen(key, picks[i], APPLIED[picks[i]]));
    const [server, at] = route(token);
    routed.push(seen(key, server, at));
    token = Math.max(token, at); // the token rises to the position read
  });
  console.log(title);
  console.log(`  naive: ${naive.join(", ")}`);
  console.log(`  token: ${routed.join(", ")} (token ${token})`);
}

In TypeScript: The tuple types of LOG and CASES keep each destructured field's type, and the leader is the fallback of ??.

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

package main

import (
	"fmt"
	"strings"
)

type write struct {
	position   int
	key, value string
}

// positions 2 to 5 change other keys
var log = []write{
	{1, "name", "Asha"},
	{6, "question", "Which mode?"},
	{7, "answer", "Use ANY 1"},
	{8, "name", "Asha Rao"},
}
var followers = []string{"A", "B"}
var applied = map[string]int{"A": 7, "B": 5}

func seen(key, server string, position int) string {
	value := "nothing"
	for _, w := range log {
		if w.position <= position && w.key == key {
			value = w.value
		}
	}
	return key + " " + value + " from " + server
}

// route: the first follower at or past the token serves the read, else the leader.
func route(token int) (string, int) {
	for _, f := range followers {
		if applied[f] >= token {
			return f, applied[f]
		}
	}
	return "leader", log[len(log)-1].position
}

func main() {
	// token, keys read, the followers a plain balancer picks
	cases := []struct {
		title string
		token int
		keys  []string
		picks string
	}{
		{"Asha reads her name after renaming at 8", 8, []string{"name"}, "B"},
		{"Ravi reads the answer, then refreshes", 0, []string{"answer", "answer"}, "AB"},
		{"A page shows an answer and its question", 0, []string{"answer", "question"}, "AB"},
	}
	for _, c := range cases {
		token := c.token
		var naive, routed []string
		for i, key := range c.keys {
			pick := c.picks[i : i+1]
			naive = append(naive, seen(key, pick, applied[pick]))
			server, at := route(token)
			routed = append(routed, seen(key, server, at))
			token = max(token, at) // the token rises to the position read
		}
		fmt.Printf("%s\n  naive: %s\n", c.title, strings.Join(naive, ", "))
		fmt.Printf("  token: %s (token %d)\n", strings.Join(routed, ", "), token)
	}
}

In Go: Go randomises map order, so the followers slice fixes the order in which route tries them; c.picks[i : i+1] is a one-letter string.

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

// positions 2 to 5 change other keys
let log: [(position: Int, key: String, value: String)] = [
  (1, "name", "Asha"), (6, "question", "Which mode?"), (7, "answer", "Use ANY 1"),
  (8, "name", "Asha Rao"),
]
let applied = [(name: "A", position: 7), (name: "B", position: 5)]
// token, keys read, the followers a plain balancer picks
let cases = [
  ("Asha reads her name after renaming at 8", 8, ["name"], "B"),
  ("Ravi reads the answer, then refreshes", 0, ["answer", "answer"], "AB"),
  ("A page shows an answer and its question", 0, ["answer", "question"], "AB"),
]

func seen(_ key: String, _ server: String, _ position: Int) -> String {
  let value = log.last { $0.position <= position && $0.key == key }?.value ?? "nothing"
  return "\(key) \(value) from \(server)"
}

/// The first follower at or past the token serves the read, else the leader.
func route(_ token: Int) -> (name: String, position: Int) {
  applied.first { $0.position >= token } ?? ("leader", log[log.count - 1].position)
}

for (title, first, keys, picks) in cases {
  var token = first
  var naive: [String] = []
  var routed: [String] = []
  for (key, pick) in zip(keys, picks) {
    let follower = applied.first { $0.name == String(pick) } ?? ("?", 0)
    naive.append(seen(key, follower.name, follower.position))
    let read = route(token)
    routed.append(seen(key, read.name, read.position))
    token = max(token, read.position)  // the token rises to the position read
  }
  print("\(title)\n  naive: \(naive.joined(separator: ", "))")
  print("  token: \(routed.joined(separator: ", ")) (token \(token))")
}

In Swift: last(where:) and first(where:) return optionals, and ?? supplies the default, nothing for a missing key and the leader for a route.

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

# positions 2 to 5 change other keys
LOG = [
  [1, 'name', 'Asha'], [6, 'question', 'Which mode?'],
  [7, 'answer', 'Use ANY 1'], [8, 'name', 'Asha Rao']
].freeze
APPLIED = { 'A' => 7, 'B' => 5 }.freeze
# token, keys read, the followers a plain balancer picks
CASES = [
  ['Asha reads her name after renaming at 8', 8, %w[name], 'B'],
  ['Ravi reads the answer, then refreshes', 0, %w[answer answer], 'AB'],
  ['A page shows an answer and its question', 0, %w[answer question], 'AB']
].freeze

def seen(key, server, position)
  found = LOG.select { |at, name, _| at <= position && name == key }.last
  "#{key} #{found ? found[2] : 'nothing'} from #{server}"
end

# The first follower at or past the token serves the read, else the leader.
def route(token)
  APPLIED.find { |_, at| at >= token } || ['leader', LOG.last.first]
end

CASES.each do |title, token, keys, picks|
  naive = []
  routed = []
  keys.zip(picks.chars).each do |key, pick|
    naive << seen(key, pick, APPLIED[pick])
    server, at = route(token)
    routed << seen(key, server, at)
    token = [token, at].max # the token rises to the position read
  end
  puts title, "  naive: #{naive.join(', ')}", "  token: #{routed.join(', ')} (token #{token})"
end

In Ruby: Hash#find yields the pairs in insertion order, and || supplies the leader when it returns nil.

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

// positions 2 to 5 change other keys
val log =
    listOf(
        Triple(1, "name", "Asha"),
        Triple(6, "question", "Which mode?"),
        Triple(7, "answer", "Use ANY 1"),
        Triple(8, "name", "Asha Rao"),
    )
val applied = linkedMapOf("A" to 7, "B" to 5)

data class Case(
    val title: String,
    val token: Int,
    val keys: List<String>,
    val picks: String,
)

fun seen(
    key: String,
    server: String,
    position: Int,
): String {
    val value = log.lastOrNull { it.first <= position && it.second == key }?.third ?: "nothing"
    return "$key $value from $server"
}

/** The first follower at or past the token serves the read, else the leader. */
fun route(token: Int): Pair<String, Int> {
    val follower = applied.entries.firstOrNull { it.value >= token }
    return follower?.toPair() ?: ("leader" to log.last().first)
}

fun main() {
    // token, keys read, the followers a plain balancer picks
    val cases =
        listOf(
            Case("Asha reads her name after renaming at 8", 8, listOf("name"), "B"),
            Case("Ravi reads the answer, then refreshes", 0, listOf("answer", "answer"), "AB"),
            Case("A page shows an answer and its question", 0, listOf("answer", "question"), "AB"),
        )
    for (c in cases) {
        var token = c.token
        val naive = mutableListOf<String>()
        val routed = mutableListOf<String>()
        for ((key, pick) in c.keys.zip(c.picks.map { it.toString() })) {
            naive += seen(key, pick, applied.getValue(pick))
            val (server, at) = route(token)
            routed += seen(key, server, at)
            token = maxOf(token, at) // the token rises to the position read
        }
        println("${c.title}\n  naive: ${naive.joinToString(", ")}")
        println("  token: ${routed.joinToString(", ")} (token $token)")
    }
}

In Kotlin: linkedMapOf keeps the insertion order that route relies on, and ?: supplies the leader when firstOrNull finds nothing.

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

import 'dart:math';

// positions 2 to 5 change other keys
const log = [
  (1, 'name', 'Asha'),
  (6, 'question', 'Which mode?'),
  (7, 'answer', 'Use ANY 1'),
  (8, 'name', 'Asha Rao'),
];
const applied = {'A': 7, 'B': 5};
// token, keys read, the followers a plain balancer picks
const cases = [
  ('Asha reads her name after renaming at 8', 8, ['name'], 'B'),
  ('Ravi reads the answer, then refreshes', 0, ['answer', 'answer'], 'AB'),
  ('A page shows an answer and its question', 0, ['answer', 'question'], 'AB'),
];

String seen(String key, String server, int position) {
  final values = [
    for (final (at, k, value) in log)
      if (at <= position && k == key) value,
  ];
  return '$key ${values.isEmpty ? 'nothing' : values.last} from $server';
}

/// The first follower at or past the token serves the read, else the leader.
(String, int) route(int token) {
  for (final MapEntry(:key, :value) in applied.entries) {
    if (value >= token) return (key, value);
  }
  return ('leader', log.last.$1);
}

void main() {
  for (var (title, token, keys, picks) in cases) {
    final naive = <String>[];
    final routed = <String>[];
    for (var i = 0; i < keys.length; i++) {
      naive.add(seen(keys[i], picks[i], applied[picks[i]]!));
      final (server, at) = route(token);
      routed.add(seen(keys[i], server, at));
      token = max(token, at); // the token rises to the position read
    }
    print('$title\n  naive: ${naive.join(', ')}');
    print('  token: ${routed.join(', ')} (token $token)');
  }
}

In Dart: Map literals keep their insertion order, and the MapEntry pattern takes each follower's name and position apart.

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

Asha reads her name after renaming at 8
  naive: name Asha from B
  token: name Asha Rao from leader (token 8)
Ravi reads the answer, then refreshes
  naive: answer Use ANY 1 from A, answer nothing from B
  token: answer Use ANY 1 from A, answer Use ANY 1 from A (token 7)
A page shows an answer and its question
  naive: answer Use ANY 1 from A, question nothing from B
  token: answer Use ANY 1 from A, question Which mode? from A (token 7)

In all three cases the plain balancer showed old or incomplete data and the token router did not. Asha’s token, 8, was ahead of both followers, so her read went to the leader. Ravi’s case is why the token also rises on reads: he had written nothing, but after seeing follower A’s state at position 7 his token was 7, so a follower behind it, such as B at 5, could no longer serve him. The router relies on two numbers that real databases expose, the position of each commit, which the leader can hand back to the client, and the position each follower has applied.

Checkpoint

Check: The position-token router

  1. Question 1 of 2 A session's token is 9,240. Follower A has applied position 9,000, follower B position 9,300, and the leader is at 9,350. Where can the read go without breaking the session's guarantees?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Follower B or the leader

    A server can serve the read if it has applied at least the token. The router tries followers first and falls back to the leader only when none qualifies, as Asha's read in the program did.

  2. Question 2 of 2 In the program, Ravi's token starts at 0 and his first read is served by follower A, which has applied position 7. What is his token after that read?

    Type a number.

    A hint

    The token rises to the position the read was served from, and never falls.

    Show the answer: Question 2 of 2

    Answer: 7 position

    max(0, 7) = 7. With a token of 7 his refresh may only go to a server that has applied 7, so B, at 5, no longer qualifies and A serves it again: the answer stays on his screen.

Practise: route reads by token

Now write the routing rule yourself; the sample tests include a follower exactly at the token and one that has the write but is unhealthy.

Exercise · Easy · JavaScript

Route a read with the session's position token

A session keeps a position token: the highest log position it has written or read. Reads that only go to followers that have applied the token give the session read-your-writes and monotonic reads. Write two functions in router.mjs.

pickServer(followers, token) gets the followers in order of preference, each as { name, applied, healthy }, where applied is the last log position the follower has applied. Return the name of the first follower that is healthy and has applied at least token. When none qualifies, return 'leader': the leader has every write.

nextToken(token, position) returns the session's token after a read from a server that had applied position. A token only ever moves forward.

  • pickServer([{ name: 'a', applied: 120, healthy: true }], 120) is 'a'
  • pickServer([{ name: 'b', applied: 95, healthy: true }], 100) is 'leader'
  • nextToken(120, 95) is 120

Starter code · router.mjs

/**
 * Where a read goes. `followers` lists { name, applied, healthy } in order of preference; `token` is the highest
 * log position the session has written or read. Return the name of the first healthy follower that has applied
 * at least `token`, or 'leader' when there is none.
 */
export function pickServer(followers, token) {
  // Replace this line with your code.
  return 'leader';
}

/** The session's token after it read from a server that had applied `position`. */
export function nextToken(token, position) {
  // Replace this line with your code.
  return token;
}
The sample tests · router.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { pickServer, nextToken } from './router.mjs';

const followers = () => [
  { name: 'a', applied: 120, healthy: true },
  { name: 'b', applied: 95, healthy: true },
  { name: 'c', applied: 130, healthy: false },
];

test('reads from the first follower that has caught up', () => {
  assert.equal(pickServer(followers(), 100), 'a');
  assert.equal(pickServer(followers(), 90), 'a');
});

test('a follower exactly at the token already has the write', () => {
  assert.equal(pickServer(followers(), 120), 'a');
});

test('skips followers that are behind or unhealthy', () => {
  const lagging = [{ name: 'b', applied: 95, healthy: true }, { name: 'a', applied: 120, healthy: true }];
  assert.equal(pickServer(lagging, 100), 'a');
  assert.equal(pickServer(followers(), 125), 'leader');
});

test('falls back to the leader', () => {
  assert.equal(pickServer([], 1), 'leader');
  assert.equal(pickServer(followers(), 500), 'leader');
});

test('a session that has seen nothing can read from any healthy follower', () => {
  assert.equal(pickServer(followers(), 0), 'a');
});

test('the token never goes backwards', () => {
  assert.equal(nextToken(100, 120), 120);
  assert.equal(nextToken(120, 95), 120);
  assert.equal(nextToken(0, 0), 0);
});
A hint

followers.find((f) => …) returns the first follower for which the test is true, or undefined when there is none, and ?? 'leader' turns undefined into the fallback. The test needs two conditions joined with &&. For the token, Math.max keeps the larger of two numbers.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 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 three anomalies, the four routing rules and the token router, one question at a time.

Practice that adapts to you

Practise this lesson

9 questions about 9 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 A dashboard shows 1,204 orders today. After a refresh it shows 1,187, and a minute later 1,230. Which guarantee is missing?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: Monotonic reads

    The refresh landed on a follower further behind than the first read's. Pinning the session to one follower, or carrying a token, keeps its reads moving forward.

  2. Question 2 of 9 What do all three lag anomalies have in common?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: One session's reads land on followers at different positions, or before its own write

    A follower shows the leader's data as it was a moment ago, which alone is harmless. The anomalies appear when one person's reads land at different positions, or at a position before that person's own write.

  3. Question 3 of 9 In the diagram, follower A has applied position 7 and follower B position 5; the answer is at 7. Ravi reads the thread from B first, then from A. Which anomaly does he see?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: None; the answer simply appears on the second read

    The order of the two reads matters: A then B goes back in time, B then A only moves forward. A guarantee is about what a session can never see, so the router must rule out the bad order, not hope for the good one.

  4. Question 4 of 9 Which of the four routing rules gives read-your-writes, monotonic reads and consistent prefix reads by construction?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: The position token

    Only the position token gives all three guarantees by construction. Its bookkeeping is small: the leader hands back each commit's position, and followers report how far they have applied.

  5. Question 5 of 9 Users edit their profile once a month and must see the edit at once; feeds must never jump back. Which pair of rules does the comparison table suggest as first steps?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: Leader after a write for the profile edit, a sticky follower for the feed

    Each rule fits a need, and a position token covers all of them at once, at the cost of carrying positions. Lag-aware routing sits beside any of them as a safety net.

  6. Question 6 of 9 How does the position-token router choose where a read goes?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: The first follower that has applied the session's token; the leader if none has

    Each write returns its commit position, the session keeps the highest position it has written or read, and the router compares that token with the position each follower has applied.

  7. Question 7 of 9 Why does the session's token rise on reads too, not only on writes?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: Otherwise a later read could go to a follower behind what the session has already seen

    Ravi had written nothing, but once he had seen follower A's state at position 7, any follower behind it would have shown him the past. The token records what the session has seen.

  8. Question 8 of 9 Follower A has applied position 7 and follower B position 5. Asha's token is 8, from her own write. How many of the two followers can serve her read?

    Type a number.

    A hint

    A follower must have applied at least the token.

    Show the answer: Question 8 of 9

    Answer: 0 followers

    Neither has: 7 and 5 are both below 8. The router sends the read to the leader, which has every write, so Asha sees her new name; the plain balancer, which picked B, showed her old one.

  9. Question 9 of 9 A follower has applied past the session's token but is failing its health checks. What should the router do?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: Skip it and try the next follower, then the leader

    The router's rule has two conditions: healthy, and applied at least the token. When no follower meets both, the leader, which has every write, serves the read.

Each anomaly reproduced, and what each fix costs Pro: opens with a Pro, Premium or Ultimate pass

A seeded model makes all three anomalies happen thousands of times, then measures each fix against the same stalls and the same users. The numbers belong to the model, and changing its lag pattern shows how they move.

In this part:

  • Reproducing the three anomalies with a seeded lag model
  • What each fix costs, read from the numbers
  • Sessions that cross tabs, devices, services and failovers

1 diagram · 1 runnable example (Python) · about 8 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Read a complete sample first: Design a unique ID generator

Lag in production: measured, alerted on and routed around Pro: opens with a Pro, Premium or Ultimate pass

Lag can be measured in two units, and each answers a different question. This part shows what PostgreSQL and MySQL report, sizes a read-from-leader window from lag percentiles, and runs a drill in which a replica stops replaying.

In this part:

  • Measuring lag in log position and in seconds
  • Sizing a read-from-leader window, with numbers
  • A failure drill: a replica that stops replaying
  • Tokens the database issues for you

1 diagram · 1 runnable example (Python) · about 10 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

  1. Fresher to mid · Warm-up

    What are read-your-writes, monotonic reads and consistent prefix reads?

    They are guarantees about the reads of one session against lagging replicas. Read-your-writes: after you write, your later reads show that write. Monotonic reads: once you have seen some state, your later reads never show an older one. Consistent prefix reads: you see writes in the order they were made, so never an answer without its question. All three can be had without making every replica current, for example by reading only from replicas that have applied the position of your latest write or read.

  2. Mid to senior

    Users report that after editing their profile, the page shows the old name for a few seconds, and sometimes the new name appears and then disappears on refresh. Reads go to three replicas behind a load balancer. Diagnose it, and fix it without sending every read to the primary.

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

  3. Senior to staff

    Your team sends a user's reads to the leader for one second after each write. Complaints about missing edits fall, but complaints that content jumps back to an older state rise. Explain why, and design a better routing rule.

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    Your system uses log-position tokens for read-your-writes. After a failover, a few users find that writes they made a minute earlier are gone, although every follower they read from had passed their token. What happened, and how do you change the design?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Lag is normal; the anomalies come from where one session’s reads land: on a follower before its own write, or on followers at different positions.
  • Read-your-writes, monotonic reads and consistent prefix reads are session guarantees, far cheaper than making every follower current.
  • A leader window and a sticky follower each give only part of the three; a position token gives all three by construction.
  • The token router reads from the first follower that has applied the session’s token, else from the leader, and raises the token to the position it read.

Terms in this lesson

consistent prefix reads
A session guarantee: a session sees writes in the order they were made, so it never sees an answer without the question it answers.
monotonic reads
A session guarantee: once a session has seen some state of the data, its later reads never show an older one, so a refresh never goes back in time.
read-your-writes
A session guarantee: after a session writes, its later reads show that write, even when they are served by followers that lag behind the leader.
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.

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.