chaowen.← Back to archive

Cloud platforms / distributed systems deep dive

Spanner commit-wait: how uncertain clocks produce an ordered global database

Distributed databases can replicate a transaction across continents, but they still need one answer to a deceptively hard question: if transaction T1 finishes before T2 starts, how does every replica know that T1 comes first? Spanner’s answer is not a perfectly synchronized clock. It is an interval-valued clock, a commit timestamp, and a deliberate wait until that timestamp is definitely in the past.

Cloud platforms / distributed systems deep dive · TrueTime intervals, external consistency, MVCC, commit-wait, open-source emulator boundaries, and a reproducible simulation

2026-09-04

The concrete problem

Suppose a banking application commits a deposit to a savings account, observes success, and then commits a debit from checking. The two transactions may be coordinated by different servers in different regions. A later snapshot read must not show the debit without the earlier deposit merely because the two servers’ wall clocks disagree.

Serializability alone is not enough. A serializable system may choose either order for concurrent transactions. Spanner’s stronger property, external consistency, requires the serial order to respect the order in which clients can observe transactions completing:

client observes: T1 commit returns ─────► T2 begins required order: timestamp(T1) < timestamp(T2) bad local-clock implementation: region A clock: 10:00:00.900 → T1 gets 900 region B clock: 09:59:59.950 → T2 gets 950 (looks fine here) with a larger offset: region A clock: 10:00:01.100 → T1 gets 1100 region B clock: 10:00:00.050 → T2 gets 50 (wrong order)

It is not enough to synchronize clocks “well.” A timestamp is useful for ordering only if the system knows how wrong it might be. Spanner makes that uncertainty explicit and then pays a bounded latency cost to remove the ambiguity before a commit becomes visible.

TrueTime is an interval, not a timestamp

TrueTime exposes an API that returns an interval containing the actual time. Write it as:

TT.now() = [earliest, latest]
epsilon    = latest - earliest

The real time is somewhere inside the interval. The important operations are conceptually:

OperationMeaningWhy Spanner cares
TT.now()Return a lower and upper bound for absolute time.A server can reason about time without pretending its local oscillator is exact.
TT.after(t)True when the lower bound has passed t; equivalently, TT.now().earliest > t.At that point, t is definitely in the past.
TT.before(t)True when the upper bound is still before t.Useful for reasoning about whether an event is definitely in the future.

TrueTime’s implementation is a trusted, highly available time service backed by clock hardware and time references. The Spanner paper describes the API and its uncertainty bound; it does not mean that ordinary application code can obtain the same guarantee from time.Now() or NTP alone.

The commit-wait proof

For a read-write transaction, Spanner chooses a commit timestamp s after the transaction has been prepared. The timestamp must be later than timestamps already required by the transaction’s participants. Before making the commit visible, the coordinator waits until TT.after(s).

1. Read/write transaction runs; participant replicas prepare. │ ▼ 2. Coordinator chooses s = a timestamp safe for this transaction. │ ▼ 3. Wait until TT.now().earliest > s ← commit-wait │ ▼ 4. Send commit / expose MVCC version at timestamp s. │ 5. A transaction starting now gets a timestamp after s.

Why does step 3 establish the ordering? Let T1 receive timestamp s1 and become externally visible only after the wait. When T2 starts after T1’s commit is observable, its server’s current interval has a lower bound later than s1. A timestamp chosen at or above T2’s current upper bound is therefore greater than s1. The system never needed the clocks to agree exactly; it needed the uncertainty bound to be honest.

At T1's commit decision:
  TT.now() = [1000, 1100]
  choose s1 = 1100
  wait until TT.now().earliest > 1100

After the wait, T2 begins:
  TT.now() = [1101, 1201]
  choose s2 >= 1201

Therefore: s1 = 1100 < s2

The wait is often short, but it is not free. If the uncertainty bound grows because time references are unavailable, an oscillator is drifting, or the system is in a degraded clock state, commit latency grows with it. That is a deliberate availability and latency tradeoff in exchange for a very strong ordering guarantee.

How MVCC turns the timestamp into a read

Spanner stores multiple immutable versions of rows. A write transaction creates a version at its commit timestamp. A read at timestamp r returns the newest committed version whose timestamp is at most r. Because commit timestamps respect externally observable order, a snapshot can be assembled across partitions without taking read locks on every row.

MechanismRoleFailure if omitted
Replica agreement / prepareEnsure participants can durably accept the transaction.A timestamp could describe a commit that some participant cannot apply.
Commit timestamp sNames the transaction’s position in the serial history.Versions from different partitions cannot be placed in one consistent order.
Commit-waitEnsures the named position is behind the real-time observation boundary.A later transaction could receive a smaller timestamp on a lagging server.
MVCC versionsAllows a past snapshot to read without blocking concurrent writes.Strong reads would need more locking or could observe mixed versions.

External consistency is stronger than ordinary serializability: it gives a serial order and constrains that order by real-time observation. It is also stronger than single-object linearizability as a database guarantee because a transaction may touch many rows and partitions.

Open-source implementation landmarks

Google does not publish the production Spanner implementation. The Cloud Spanner Emulator is an open-source, correctness-focused local implementation of the API and transaction semantics. It is valuable for tracing the application-visible transaction path, but it is not a miniature multi-region Spanner and does not reproduce TrueTime’s clock infrastructure.

Source locationImportant codeWhat it shows
frontend/handlers/transactions.ccBeginTransaction, Commit, RollbackMaps the gRPC API to session and transaction objects, calls txn->Commit(), and returns the commit timestamp.
backend/transaction/read_write_transaction.ccReadWriteTransactionConnects read/write operations to locking, buffered writes, action processing, flushing, and transaction resolution.
backend/transaction/transaction_store.ccAcquireReadLock, AcquireWriteLock, BufferWriteOp, ReadShows how the emulator protects keys, overlays buffered mutations on base storage, and keeps uncommitted state separate from committed rows.
backend/transaction/commit_timestamp.ccCommitTimestampTracker, sentinel replacementImplements allow_commit_timestamp column behavior and replaces pending sentinels during flush; this is commit-timestamp data semantics, not a public TrueTime implementation.
tests/conformance/casestransaction and commit-timestamp testsExecutable API behavior and error contracts. The emulator README explicitly warns that it is in-memory and not a performance or production-consistency model.

The boundary matters when testing: an emulator can validate retries, transaction state transitions, SQL behavior, and commit-timestamp columns, but a passing local test does not validate regional quorum latency, clock uncertainty, replica failure behavior, or commit-wait duration.

Tradeoffs and failure modes

Debugging a suspicious ordering or latency problem

  1. Reconstruct client-observed order. Record request start, commit response, retry count, region, and returned commit timestamp. The relevant assertion is about when a client observed completion, not merely when two RPCs were sent.
  2. Separate retries from commit-wait. A high end-to-end latency may be lock contention, an aborted transaction, cross-region coordination, or uncertainty-driven wait. Instrument transaction attempts individually.
  3. Check the read timestamp mode. A stale or bounded-staleness read intentionally trades freshness for latency. Do not compare it with a strong read as if both promised the same snapshot boundary.
  4. Inspect the commit timestamp. In a controlled test, issue T1, wait for its successful response, then issue T2 and compare returned timestamps. A correct external-consistency implementation must order them even when they use different sessions.
  5. Test the full retry closure. Force an abort or transient transport failure and verify that the application reruns the whole transaction and does not replay an external side effect between attempts.
  6. Do not use the emulator to explain production time behavior. Use it for API and semantic tests, then run a small production-like staging test for region placement, failover, and latency distributions.

Compact lab: simulate the proof

Goal: produce a deterministic counterexample for unsafely ordering transactions with local clocks, then show how an interval and commit-wait establish the order.

Prerequisites: Python 3.8 or newer. No cloud account, database, or third-party package is required.

python3 - <<'PY'
EPSILON = 100

def truetime(real_ms):
    return (real_ms - EPSILON, real_ms + EPSILON)

def naive_local_clock(real_ms, offset_ms):
    return real_ms + offset_ms

# T1 completes at real time 1000 on a fast-clock server.
# T2 starts later, at real time 1001, on a slow-clock server.
t1_naive = naive_local_clock(1000, +100)
t2_naive = naive_local_clock(1001, -100)
print("naive local timestamps:", t1_naive, t2_naive)
print("naive order is valid:", t1_naive < t2_naive)

# TrueTime lets T1 choose an upper-bound timestamp, then waits until
# the lower bound has passed it before exposing the commit.
t1_interval = truetime(1000)
s1 = t1_interval[1]
visible_at = s1 + EPSILON + 1
t2_interval = truetime(visible_at)
s2 = t2_interval[1]

print("T1 interval:", t1_interval, "s1:", s1)
print("T1 visible after commit-wait at real time:", visible_at)
print("T2 interval:", t2_interval, "s2:", s2)
print("TrueTime order is valid:", s1 < s2)
PY

Expected result:

naive local timestamps: 1100 901
naive order is valid: False
T1 interval: (900, 1100) s1: 1100
T1 visible after commit-wait at real time: 1201
T2 interval: (1101, 1301) s2: 1301
TrueTime order is valid: True

The simulation is intentionally conservative: with an uncertainty of 100 ms, it waits until the lower bound is strictly past 1100. It demonstrates the ordering proof, not Google’s clock hardware, replica protocol, or production latency.

Revealing variations

python3 - <<'PY'
for epsilon in (100, 10, 1):
    t1_real = 1000
    s1 = t1_real + epsilon
    visible_at = s1 + epsilon + 1
    print(f"epsilon={epsilon:3}ms  commit-wait={visible_at - t1_real:3}ms")
PY

Expected variation: epsilon=100ms commit-wait=201ms, epsilon=10ms commit-wait=21ms, and epsilon=1ms commit-wait=3ms. The second variation changes the time at which T2 begins: start it before visible_at and the proof does not apply, because T2 is not yet causally after an externally visible T1. If the here-document is unavailable in your shell, paste either Python block into a file and run python3 /path/to/truetime-lab.py. No local Spanner emulator is required for this lesson.

Production lesson: a globally ordered database does not require magical clocks. It requires a clock bound that the system can trust, a protocol that waits until the bound no longer overlaps the chosen timestamp, and storage semantics that make that timestamp the version boundary. The cost is visible: uncertainty becomes commit latency, and clock health becomes part of database correctness.

Primary sources