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.
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:
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:
| Operation | Meaning | Why 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).
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.
| Mechanism | Role | Failure if omitted |
|---|---|---|
| Replica agreement / prepare | Ensure participants can durably accept the transaction. | A timestamp could describe a commit that some participant cannot apply. |
Commit timestamp s | Names the transaction’s position in the serial history. | Versions from different partitions cannot be placed in one consistent order. |
| Commit-wait | Ensures the named position is behind the real-time observation boundary. | A later transaction could receive a smaller timestamp on a lagging server. |
| MVCC versions | Allows 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 location | Important code | What it shows |
|---|---|---|
frontend/handlers/transactions.cc | BeginTransaction, Commit, Rollback | Maps the gRPC API to session and transaction objects, calls txn->Commit(), and returns the commit timestamp. |
backend/transaction/read_write_transaction.cc | ReadWriteTransaction | Connects read/write operations to locking, buffered writes, action processing, flushing, and transaction resolution. |
backend/transaction/transaction_store.cc | AcquireReadLock, AcquireWriteLock, BufferWriteOp, Read | Shows how the emulator protects keys, overlays buffered mutations on base storage, and keeps uncommitted state separate from committed rows. |
backend/transaction/commit_timestamp.cc | CommitTimestampTracker, sentinel replacement | Implements allow_commit_timestamp column behavior and replaces pending sentinels during flush; this is commit-timestamp data semantics, not a public TrueTime implementation. |
tests/conformance/cases | transaction and commit-timestamp tests | Executable 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
- Uncertainty inflation. Commit-wait latency follows the uncertainty bound. Monitor it separately from lock wait and network latency; otherwise clock degradation can look like a generic database slowdown.
- Aborted transactions. Timestamp ordering does not remove write conflicts. A transaction can still be aborted by concurrency control and must be retried according to the client library’s rules.
- Long transactions. Holding locks or read/write intents for a long time increases contention. Keep transactions small and retry the complete transaction body, not just the final RPC.
- Idempotent commit handling. A client can lose the response after the service commits. Commit APIs and client libraries need safe retry behavior; application mutation identifiers should not cause duplicate business effects.
- Emulator overconfidence. The emulator is in-memory, permits only one read-write transaction or schema change at a time, and ignores some production limits. It is a semantic test double, not a latency or availability benchmark.
- Clock infrastructure is part of the trusted base. If a time service reports an interval that does not contain reality, the proof collapses. Time-source health is therefore a correctness signal, not merely an operations metric.
- Timestamp visibility. Commit timestamps expose ordering information. Avoid treating them as secret identifiers or as a substitute for an authorization decision.
Debugging a suspicious ordering or latency problem
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
Primary sources
- Corbett et al., “Spanner: Google’s Globally-Distributed Database,” OSDI 2012 — architecture, TrueTime API, commit-wait, MVCC, and external consistency. The page links to the paper PDF.
- Google Cloud Spanner documentation: TrueTime and external consistency — the application-facing explanation of timestamp ordering, MVCC, strong reads, and the difference between external consistency and serializability.
- Cloud Spanner Emulator repository — scope, build instructions, architecture, supported features, and explicit limitations of the open-source local emulator.
- Emulator transaction handlers — the public Commit/Rollback API path and transaction state checks.
- Emulator transaction store — lock acquisition, buffered mutations, and overlay reads.
- Emulator commit-timestamp implementation — pending commit-timestamp sentinels, validation, and replacement during transaction processing.