---
id: 25
title: "Track the version a client has seen for read-your-writes"
status: current
date: 2026-06-12
authors:
  - "Theo Zourzouvillys"
tags: [architecture, data, consistency, api]
summary: "For read-your-writes across backends, track the latest version a client has seen — a token or vector clock. Return it on write; reads then go to a backend at least that fresh. Hold it client-side (a token they present) or server-side (a gateway tracks the session and routes)."
supersedes: null
superseded_by: null
aliases: []
---

## TL;DR

Once state is replicated or spread across multiple backends — read replicas, derived stores fed
asynchronously ([ZFN-24](/zfn/24-one-transactional-store-per-write/)) — a client can write, then read
from a backend that hasn't caught up yet and not see its own change. If you need **read-your-writes**
(read-after-write) consistency, you need a way to know **the latest version of state a client has
already observed**, so a later read can be served from a backend that is **at least that fresh** (route
to a caught-up replica, wait for it, or fall back to the primary).

Represent "what this client has seen" as a **version token** — a single log position/LSN for one
replicated store, or a **vector clock** (a version per backend system) when several systems each have
their own version. Carry it one of two ways: **client-side**, where the client holds the token (a
cookie/header it presents on the next request), or **server-side**, where the **ingress gateway tracks
the session's version** and routes reads to backend instances that are caught up. Only pay for it on the
reads that actually need the guarantee.

## Context

Strong single-store consistency is easy to take for granted until you scale out reads or derive other
systems from an event stream. Then the classic bug appears: a user saves a change, the write commits to
the primary, the UI reloads, the read lands on a replica or a derived store that's a few hundred
milliseconds behind — and their change is "gone." It comes back a moment later, which makes it
intermittent and maddening. It's not a write bug; it's a *read* routed to a backend that hasn't seen the
write yet.

The fix isn't to make everything synchronously consistent (you gave that up on purpose, for
availability and to avoid 2PC — [ZFN-24](/zfn/24-one-transactional-store-per-write/)). It's to make
reads **aware of a freshness floor**: the client knows it has already observed version *N*, so any read
it does must come from a backend at version ≥ *N*. The only new requirement is a way to *name* the
version a client has seen and to carry it from the write to the subsequent reads.

For a single replicated store, that version is one monotonic value (a log sequence number / commit
position). When read-your-writes must span **several** independently-versioned systems, one number isn't
enough — you need a **vector clock**: a small map of *system → version this client has seen*, so each
backend can be checked against its own component.

## Recommendation

**Name the version a client has seen, carry it, and serve reads from a backend at least that fresh —
and only where it matters.**

- **Return a version token on writes.** Every write response carries the version(s) it produced — an LSN
  for a single store, or a vector across the systems it touched.
- **Require the token on reads that need read-your-writes.** A read presents the token; the system
  guarantees it reads from a backend at ≥ that version by one of: **routing** to a replica known to be
  caught up, **waiting** briefly for the chosen replica to reach it, or **falling back to the primary**.
  Reads that don't need the guarantee skip it and stay cheap/fast on any replica.
- **Choose where the token lives:**
  - **Client-side.** The client holds the token — a cookie, a header, an opaque field echoed back — and
    presents it on subsequent requests. The client explicitly opts into the stronger semantics by
    carrying its position; the server stays stateless about sessions. (This is how consistency tokens in
    several distributed databases work.)
  - **Server-side.** The **ingress gateway** keeps the vector clock for each session and routes that
    session's reads to backend instances/replicas that satisfy it — read-your-writes as a *routing*
    decision in the data plane ([ZFN-16](/zfn/16-separate-data-plane-control-plane/)), transparent to the
    client. Costs session state and stickiness at the gateway, buys a clean client contract.
- **Use a vector clock when, and only when, you span multiple versioned systems.** One backend → one
  monotonic token is simpler; reserve the vector for genuine multi-system read-your-writes.
- **Bound the cost.** Routing/waiting for freshness adds latency and can push load to the primary; apply
  it to the specific reads that need it (right after a write, the user's own view), not globally.

**Scope.** This buys **read-your-writes / monotonic-reads** for a client's own session — not global
linearizability. Two *different* users still see eventual consistency relative to each other, which is
usually exactly what you want.

## Consequences

**Easier:**

- The "I just saved it and it's gone" bug disappears for the user who made the change, without forcing
  global strong consistency.
- You keep the availability and simplicity of replicas / async-derived stores
  ([ZFN-24](/zfn/24-one-transactional-store-per-write/)) and add freshness only where it's needed.
- The token makes "how stale is acceptable here?" an explicit, per-read decision instead of a hope.

**Harder:**

- Tokens must be threaded correctly end to end; a dropped token silently loses the guarantee, which is
  hard to notice in testing and shows up as the intermittent bug you were trying to kill.
- Server-side tracking adds session state and routing/stickiness at the gateway; client-side adds a
  contract clients must honor.
- Vector clocks across many systems get fiddly to produce, compare, and bound in size — keep the set of
  tracked systems small.
- Waiting-for-freshness adds tail latency, and primary fallback adds load; both need limits.

## References

- [ZFN-24](/zfn/24-one-transactional-store-per-write/) — async propagation to derived stores is what
  creates the read-your-writes gap this note closes.
- [ZFN-16](/zfn/16-separate-data-plane-control-plane/) — the gateway tracking session versions and
  routing reads is a data-plane responsibility.
- [ZFN-12](/zfn/12-queues-topics-journals/) — replicas and derived consumers lag the journal; the token
  is how a read knows it's caught up.
- Read-your-writes / monotonic-reads session guarantees, and database consistency tokens, as worked
  designs.

## Changelog

- **2026-06-12**: First published as a Field Note.
