Lesson 8: The Trouble with Distributed Systems

Lesson 0008 — 55 min read

The last few chapters were too optimistic. Chapter 8 is the inversion: assume everything that can go wrong will go wrong. Networks drop, reorder, and delay packets arbitrarily. Clocks lie. Processes pause for minutes at a time. A node cannot even reliably tell whether another node is alive or dead. This chapter is the pessimistic foundation on which every distributed algorithm in Chapter 9 is built.

This chapter matters because it rewires how you think about distributed systems. Before Chapter 8, it is tempting to treat the network as a reliable pipe and clocks as truth. After Chapter 8, you understand that every message is a guess, every timeout is a gamble, and every lock is a lease that can expire while you are still holding it.

Mission tie-in: every system design interview question — design Twitter, WhatsApp, Uber, Dropbox — hinges on the assumptions you make about network reliability, clock accuracy, and process pauses. The best FAANG candidates don't just propose architectures; they identify which partial failures could break their design and how to tolerate them.


Faults and Partial Failures

On a single computer, software is deterministic: the same operation always produces the same result. If hardware fails (memory corruption, loose connector), the result is usually a total system failure — kernel panic, blue screen, failure to start. That is a deliberate design choice: computers prefer to crash entirely rather than return a wrong result, because wrong results are harder to debug.

In a distributed system, this changes fundamentally. There is no single point of total failure. Instead, some parts break while others keep working. This is a partial failure: one node is down, another is fine, a third is slow, and the network between them is flaky. Partial failures are nondeterministic — the same operation may work sometimes and fail unpredictably other times.

The core difficulty of distributed systems is not scale, not performance, but this nondeterminism. You cannot test for every interleaving of partial failures. You must design for them.

Core insight: distributed systems are hard because partial failures are nondeterministic. The same code path may succeed a thousand times and fail on the thousand-and-first due to a coincidental network hiccup, clock skew, or GC pause. Designing for this reality requires a fundamentally different mindset than single-machine programming.


Cloud Computing vs Supercomputing

Two philosophies sit at opposite ends of large-scale computing.

Dimension Supercomputing (HPC) Cloud Computing
Failure handling Checkpoint → stop entire cluster → repair → restart from checkpoint Tolerate partial failures; degrade gracefully
Service model Offline (batch jobs) Online (must serve users with low latency)
Hardware Specialized, reliable nodes; RDMA, shared memory Commodity machines; higher failure rates but lower cost
Network Specialized topologies (mesh, torus) IP/Ethernet in Clos topologies
Node count Small enough that one failure is rare Large enough that something is always broken
Geographic spread One location Often multi-datacenter, global

This book focuses on the cloud/internet-services end. The key implication is simple but profound: in a system with thousands of nodes, something is always broken. If your error-handling strategy is "stop and restart," your system will spend most of its time recovering. Instead, you must tolerate failures at the node level — rolling upgrades, single-node restarts, traffic shifting — without disrupting the whole service.


Building Reliable Systems from Unreliable Components

It seems counterintuitive: how can a system be more reliable than its least reliable component? Yet this is an old and proven idea:

The key is that each layer handles some low-level faults so the layer above can reason about the remaining ones. But there is a limit: TCP cannot fix unbounded network delays; replication cannot fix a corrupted application protocol. This is the end-to-end argument (returned to in Chapter 10): higher-level correctness often cannot be fully delegated to lower layers.


Unreliable Networks

The distributed systems in this book are shared-nothing: machines communicate only through a network. The internet and datacenter networks are asynchronous packet networks — they give no guarantees about when (or whether) a packet will arrive.

When you send a request and expect a response, many things can go wrong:

  1. Your request was lost (unplugged cable, misconfigured switch).
  2. Your request is queued and will be delivered later (overloaded recipient or network).
  3. The remote node crashed.
  4. The remote node is paused (long GC pause, VM suspension) and will resume later.
  5. The remote node processed your request, but the response was lost.
  6. The remote node processed your request, but the response is delayed.

Frustrating reality: if you send a request and get no response, you cannot distinguish case (a) from case (b) from case (c). The only information you have is that you haven't received a response yet. This fundamental ambiguity is the source of most distributed systems complexity.

The standard answer is a timeout: after some time, assume the response won't arrive. But a timeout does not resolve the ambiguity — it just draws a line after which you act as if the request failed. The request may still be sitting in a queue somewhere, waiting to be delivered and executed long after you gave up.


Network Faults in Practice

One might hope that decades of networking experience would have made networks reliable. Data shows otherwise. Studies of medium-sized datacenters found ~12 network faults per month, half disconnecting a single machine and half disconnecting an entire rack. Redundant networking gear does not reduce faults as much as expected because human error (misconfiguration) is a major cause.

Public clouds like EC2 are notorious for transient network glitches. Even well-managed private datacenters are not immune: software upgrades on switches can trigger topology reconfiguration with minute-long delays. Sharks bite undersea cables. Network interfaces can fail in one direction only.

A network partition (or netsplit) occurs when one part of the network is cut off from another. This book uses "network fault" to avoid confusion with data partitioning (Chapter 6). Your software must handle these faults — even if they are rare — because unhandled faults can lead to deadlocks, unrecoverable states, or data deletion.

Handling faults doesn't mean always tolerating them. Sometimes showing an error message is acceptable. But you need to know what your system does under network faults. Testing with deliberate fault injection (Chaos Monkey) is standard practice.


Detecting Faults

Many systems need to detect faulty nodes automatically: load balancers stop sending requests to dead nodes; single-leader databases promote a follower when the leader fails. But the network's uncertainty makes fault detection fundamentally hard.

In specific circumstances you get explicit feedback:

But you cannot rely on any of these. The only universal detection mechanism is a timeout. The problem then becomes: how long should the timeout be?


Timeouts and Unbounded Delays

A long timeout means users wait a long time (or see errors) before failover. A short timeout means you risk declaring a node dead when it is merely slow — which can cause double execution, cascading failures, and split-brain.

The ideal scenario would be a network with guaranteed maximum delay d and a node that always processes requests within time r. Then timeout = 2d + r would be safe. But real networks are asynchronous: delays are unbounded. No matter how fast the network is most of the time, a transient spike can throw off a low timeout.

Network congestion and queueing

The main source of delay variability is queueing:

In public clouds and multi-tenant datacenters, a noisy neighbor (another customer using the same shared infrastructure) can cause highly variable network delays. You cannot control or predict this. The only honest approach is to measure round-trip times experimentally over an extended period, then choose a timeout that balances failure detection speed against false-positive risk.

Sophisticated systems use adaptive timeouts: the Phi Accrual failure detector (used in Akka and Cassandra) continuously measures response time distribution and adjusts timeouts accordingly. TCP itself does this with its retransmission timeout estimator.


Synchronous vs Asynchronous Networks

Why can't we make computer networks as reliable as the telephone network? The telephone network is circuit-switched: when you make a call, a fixed bandwidth allocation is reserved end-to-end for the duration. There is no queueing because the capacity is guaranteed. Delays are bounded and predictable.

Datacenter networks and the internet are packet-switched. They optimize for bursty traffic — web pages, emails, file transfers — where reserving bandwidth ahead of time would waste capacity. TCP dynamically adapts to available bandwidth. The trade-off is that queueing and unbounded delays are inherent.

Variable delays are not a law of nature. They are a cost/benefit trade-off: statically partitioned resources (circuits) give latency guarantees but lower utilization (more expensive); dynamically partitioned resources (packets) give higher utilization but variable delays.

Mental model: packet switching maximizes utilization by letting senders compete for bandwidth. The cost is that you cannot bound delays. Circuit switching guarantees latency but leaves capacity idle. Most internet services choose the cheaper, variable-delay option and design their software to cope.


Unreliable Clocks

Distributed systems depend on clocks for timeouts, response time measurement, rate calculation, event ordering, cache expiry, and log timestamps. But clocks are not reliable. Each machine has a quartz crystal oscillator that drifts — it runs slightly faster or slower than real time.

Modern computers have at least two kinds of clocks:

Time-of-day clocks

Return wall-clock time since the epoch (e.g., System.currentTimeMillis()). Synchronized with NTP, but subject to:

Time-of-day clocks are not suitable for measuring elapsed time because of these jumps. Using them for ordering events across nodes is especially dangerous.

Monotonic clocks

Return a value that is guaranteed to move forward (e.g., System.nanoTime()). Suitable for measuring durations — timeouts, response times — because they only move forward. The absolute value is meaningless (e.g., nanoseconds since boot). You cannot compare monotonic clock values from different machines.

NTP may slew (speed up or slow down) monotonic clocks by up to 0.05%, but cannot cause them to jump.


Clock Synchronization and Accuracy

Getting clocks to agree is harder than it seems:

Key consequence: incorrect clocks are silent. A broken CPU = no boot. A broken clock = everything works fine for months until subtle data loss occurs. If your software relies on synchronized clocks, you must monitor clock offsets and remove nodes that drift too far.


Timestamps for Ordering Events

Using time-of-day clocks for ordering events across nodes is tempting but dangerous. The chapter's canonical example: two clients write to a multi-leader database. Client A writes x = 1 at timestamp 42.004 on node 1. Client B writes x = 2 on node 3 (which is causally later) but gets timestamp 42.003 because node 3's clock is slightly behind. Node 2 receives both writes and incorrectly concludes that x = 1 is newer, dropping x = 2.

This is last write wins (LWW) conflict resolution, used in Cassandra and Riak. LWW causes writes to silently disappear when clocks are skewed — even by milliseconds. Additional problems:

The safer alternative is logical clocks — counters that order events by causality, not by physical time. Lamport clocks and version vectors track happens-before relationships without relying on clock synchronization. These are covered more deeply in Chapter 9.


Clock Confidence Intervals and TrueTime

A clock reading is not a point in time — it is a range of times within a confidence interval. You might be 95% confident the true time is between 10.3 and 10.5 seconds, but no more precise. Most systems don't expose this uncertainty.

Google's Spanner is the notable exception. Its TrueTime API returns [earliest, latest] — the earliest and latest possible current times. Spanner uses GPS receivers and atomic clocks in each datacenter to keep the uncertainty interval around 7 ms. When committing a read-write transaction, Spanner deliberately waits for the length of the confidence interval before committing. This ensures that the transaction's timestamp is definitely in the past from any future transaction's perspective, enabling distributed snapshot isolation without a centralized coordinator.

Deep insight: Spanner solves distributed transaction ordering by accepting clock uncertainty rather than pretending it doesn't exist. The wait-for-confidence-interval trick is only feasible because Google invests heavily in clock hardware (GPS + atomic clocks). This is not a general-purpose solution — it is an architectural statement: "we can afford physical clock precision that no one else can."


Process Pauses

A node can be paused for a significant length of time at any point in its execution. This is not hypothetical. Here are real causes:

  1. Garbage collection (GC): stop-the-world GC pauses in the JVM have been known to last minutes. Even concurrent collectors stop the world for some phases.
  2. VM suspension: the hypervisor pauses the VM to live-migrate to another host, or shares CPU with other VMs (steal time).
  3. Laptop lid close: end-user devices are suspended and resumed arbitrarily.
  4. Context switching: the OS can pause a thread at any instruction boundary, for any duration if the system is under load.
  5. Synchronous disk I/O: a page fault, lazy class loading, or disk access can pause a thread for disk latency. With network filesystems like EBS, this latency is itself variable.
  6. Swapping/paging: memory pressure causes page faults that load pages from disk, potentially triggering thrashing.
  7. SIGSTOP: an operator can accidentally pause a process.

The problem is not that pauses happen (they do). The problem is that the paused node does not know it was paused. When it resumes, it thinks no time has passed. It may still believe it holds a lease, a lock, or a leadership role — all of which may have expired and been reassigned to other nodes.

This is the distributed-systems analogy of race conditions in multithreaded code. The difference: in multithreaded code you have shared memory and mutexes. In distributed systems, you only have messages sent over an unreliable network.

Core mantra: a node in a distributed system must assume it can be paused for any duration at any point. It cannot trust its own sense of time. It must use external mechanisms — fencing tokens, quorum consensus, watchdogs — to verify that its view of the world is still valid.

Response time guarantees and real-time systems

Hard real-time systems (aircraft, rockets, car airbags) eliminate unbounded pauses through: real-time operating systems, guaranteed CPU scheduling, documented worst-case execution times, and restricted memory allocation. But this requires massive investment and severely limits programming language and tool choices.

Most server-side data systems operate in a non-real-time environment. The practical approach is not to eliminate pauses but to limit their impact:


Knowledge, Truth, and Lies

A node cannot know anything for sure. It can only guess based on messages received. If a remote node does not respond, there is no way to distinguish between:

This uncertainty has philosophical flavor. What does it mean for a node to "know" something in a distributed system? The answer is pragmatic: we define a system model — a set of assumptions about what can happen — and design algorithms that are provably correct within that model, even under worst-case conditions.


The Truth Is Defined by the Majority

A node cannot trust its own view of the world. Imagine a node that can receive all messages but its outgoing messages are all dropped (asymmetric network fault). Other nodes time out and declare it dead. From its perspective, it is alive and receiving requests — but it has no way to prove it.

Similarly, a node that experiences a long GC pause may wake up to find itself declared dead and replaced. It may feel perfectly healthy, but the other nodes have already elected a new leader, reassigned its partitions, and moved on.

The solution is quorum: decisions require votes from a minimum number of nodes. If a majority of nodes declares another node dead, that node must accept the decision, even if it believes itself alive. Majority quorums are safe because there can only be one majority at any time — no two majorities can make conflicting decisions.


The Leader and the Lock

Many distributed systems require that only one node holds a particular role at any time: the leader for a database partition, the lock holder for a resource, the owner of a username. The danger is that a node believes it is the chosen one, but no quorum agrees.

The classic failure mode: a client acquires a lease (lock with timeout) from a lock service. Before it finishes its work, a GC pause causes it to miss the lease renewal deadline. The lease expires and another client acquires it. The first client wakes up, still believes it holds the lease, and writes to the resource simultaneously with the second client. Result: data corruption.

Fencing tokens

The fix is fencing tokens. Every time the lock service grants a lease, it also returns a monotonically increasing token number. Every write request to the storage service must include this token. The storage service tracks the highest token it has seen and rejects any write with a lower token:

Fencing tokens require the resource (storage service) to actively check them. You cannot rely on clients to self-police — clients are untrustworthy by nature. ZooKeeper's zxid and node version cversion are commonly used as fencing tokens.

Interview-ready formulation: "To prevent a delayed client from corrupting data after its lease expires, I would use fencing tokens — monotonically increasing numbers granted with each lock. The protected resource checks every write against the last-seen token and rejects stale ones. This moves the safety check from the client (untrustworthy) to the resource (authoritative)."


Byzantine Faults

So far we have assumed nodes are unreliable but honest — they may be slow, dead, or outdated, but they follow the protocol. Byzantine faults are when nodes lie: they send arbitrary, corrupted, or malicious messages. This is the Byzantine Generals Problem: n generals must agree on a battle plan while some are traitors actively sending false information.

Byzantine fault tolerance is relevant in:

In most server-side datacenter systems, Byzantine faults are assumed not to exist because: all nodes are controlled by the same organization, radiation levels are low, and Byzantine-tolerant protocols are complex and expensive.

However, weak forms of lying are worth guarding against even within a trusted datacenter:


System Models: Formalizing Assumptions

To design provably correct distributed algorithms, we formalize the faults we expect. Three timing models:

Model Assumption Realistic?
Synchronous Bounded network delay, bounded process pauses, bounded clock error No — unbounded delays and pauses do occur
Partially synchronous Mostly synchronous, but bounds may be violated occasionally (and arbitrarily) Yes — this is real life
Asynchronous No timing assumptions; no clock; no timeouts allowed Too restrictive for most practical algorithms

Three node failure models:

Model Behavior Used for
Crash-stop Node fails by crashing permanently; never comes back Simpler theoretical models
Crash-recovery Node can crash and restart; stable storage survives crashes; memory state is lost Most practical systems
Byzantine (arbitrary) Node may do anything, including lying Security-critical, multi-org, aerospace

Most useful combination for real systems: partially synchronous timing + crash-recovery faults. This is the model underlying algorithms like Raft, Zab, and Viewstamped Replication.


Safety and Liveness

To define correctness of a distributed algorithm, we describe its properties. For example, a fencing token generator:

Uniqueness and monotonic sequence are safety properties: "nothing bad happens." Availability is a liveness property: "something good eventually happens" (note the word "eventually").

The distinction is crucial for building reliable algorithms:

The partially synchronous model typically requires that safety holds unconditionally, while liveness holds only during periods of synchrony. This is why consensus algorithms (Paxos, Raft) guarantee safety even under network partitions but may stop making progress until the partition heals.


Summary: The Full Inventory of Troubles

Category Core problem Key mitigations
Unreliable networks Packets lost, delayed, duplicated, reordered. No way to distinguish node failure from network failure. Timeouts (experimentally determined); circuit breakers; retry with backoff; idempotency
Unreliable clocks Clocks drift, jump, and disagree. NTP has limited accuracy. LWW with physical clocks loses data. Monotonic clocks for durations; logical clocks for ordering; monitor clock offsets; remove drifting nodes
Process pauses GC, VM suspension, swapping, SIGSTOP can pause a node for arbitrarily long. The node doesn't know it was paused. Fencing tokens; coordinated GC pauses; rolling restarts; assume worst-case pause duration
Knowledge uncertainty A node cannot trust its own view. Majority vote defines truth. Quorum prevents conflicting decisions. Quorum-based decisions; system models with explicit fault assumptions
Byzantine faults Nodes may lie or send corrupted data. Application-level checksums; input validation; multiple NTP servers; full BFT protocols for extreme cases

The defining characteristic of distributed systems is partial failure. Unlike single-machine software, where the answer is either "yes" or "no, and here is a stack trace," distributed systems live in an ambiguous gray zone where a request may be processed, partially processed, not processed at all, or processed multiple times — and you may never know which.

Chapter 9 builds on this pessimistic foundation. Given that everything can fail in so many ways, how can algorithms like Paxos, Raft, and Zab still provide strong guarantees? The answer is system models, safety/liveness trade-offs, and carefully designed protocols.


Retrieval Quiz

1. What makes distributed systems fundamentally different from single-computer programming?

Partial failures are nondeterministic and hard to test for Distributed systems use fundamentally different hardware Distributed systems have more data to process

2. When you send a request and get no response, what can you conclude?

Nothing beyond "no response received yet" That the remote node is down after a timeout An error packet telling you exactly what happened

3. What is the best kind of clock for measuring elapsed time (timeouts)?

Monotonic clock Time-of-day clock Any physical clock

4. Why is last-write-wins (LWW) dangerous in multi-leader replication?

Clock skew causes causally later writes to be silently dropped It crashes nodes with wrong timestamps It creates conflicts that require manual resolution

5. What is a process pause and why is it dangerous?

A node is suspended arbitrarily; it resumes without knowing it was paused, believing old state is still valid The same as a network partition The node always detects the pause and logs it

6. How do fencing tokens prevent stale-leader corruption?

The protected resource rejects writes with stale tokens by tracking the highest token seen The lock service checks tokens before granting a lease The client checks its own token before writing

7. In the partially synchronous model, when does liveness hold?

Safety holds always; liveness holds only during periods of synchrony Liveness holds always, safety holds only during synchrony Neither safety nor liveness is guaranteed

8. Why can't we make datacenter networks have bounded delays like telephone circuits?

Packet switching maximizes utilization for bursty traffic; circuits waste capacity but bound delays The hardware is incapable of bounded delays Bounded delays are not useful for distributed systems

9. A clock reading has a confidence interval. What does Spanner do with this?

Waits for the interval before committing so timestamps are definitely in the past Ignores the uncertainty and uses the midpoint timestamp Uses last-write-wins with a tiebreaker

10. What is the single most important mindset shift this chapter teaches?

Suspicion, pessimism, and paranoia: design assuming every message, clock, and process can fail arbitrarily Buy better hardware to avoid failures Avoid building distributed systems whenever possible

Notes

Distributed systems definition :-

Partial failures are nondeterministic → hardest part of DS

Single computer = either works or crashes deterministically

DS = some parts broken, others fine, and you cant tell which

HPC v/s Cloud :-

HPC → checkpoint whole cluster, restart on failure

Cloud → tolerate partial failure, degrade gracefully, rolling upgrades

Unreliable network :-

Async packet network = no guarantees on delivery or timing

If you send request + get no response → cannot distinguish:

a) request lost

b) node down

c) response lost/delayed

Timeout is only universal answer, but timeouts are guesses

Network queueing :-

Switch queues + OS queues + VM pauses + TCP flow control + retransmissions

All contribute to unbounded delay variability

Adaptive timeouts (Phi Accrual) adjust based on observed distribution

Circuit v/s Packet :-

Circuit (phone network) = reserved bandwidth, bounded delay, lower utilization

Packet (internet) = dynamic sharing, unbounded delay, higher utilization

Trade-off is cost vs latency predictability

Clocks :-

Time-of-day :-

Wall clock (eg System.currentTimeMillis)

Can jump backward/forward due to NTP resets

NOT for elapsed time, dangerous for ordering

Monotonic :-

Always moves forward (eg System.nanoTime)

For durations and timeouts ✓

Absolute value meaningless; cannot compare across machines

LWW problem :-

Clock skew → causally later write can get earlier timestamp

Later write gets silently dropped → data loss with no error

Solution = logical clocks (Lamport, version vectors) not physical time

TrueTime (Spanner) :-

Clock reading = confidence interval [earliest, latest], not a point

Spanner waits for interval before commit → timestamp definitely in past

Requires GPS + atomic clocks per DC (~7ms uncertainty)

Process pauses :-

GC pauses can last minutes

VM suspension, context switching, swapping, SIGSTOP

Node does not know it was paused → resumes with expired lease

Mitigations → fencing tokens, coordinated GC, rolling restarts

Fencing tokens :-

Lock service grants monotonically increasing token with each lease

Storage tracks highest token seen, rejects lower

Moves safety from client (untrustworthy) to resource (authoritative)

ZooKeeper zxid / cversion work as tokens

Truth := majority :-

Node cannot trust own view → quorum defines truth

Asymmetric fault → node receives but cannot send → declared dead

GC pause → node alive but declared dead → must accept demotion

Only one majority exists at any time → safe

Byzantine faults :-

Nodes may lie / send arbitrary messages

BFT = n generators, up to 1/3 can be traitors

Not assumed in datacenter (single org + low radiation)

Weak lying still worth guarding (checksums, input validation, multi-NTP)

Timing models :-

a) Synchronous → bounded delay/pause/error → unrealistic

b) Partially synchronous → most useful model; bounds hold most of the time

c) Asynchronous → no timing assumptions → too restrictive

Failure models :-

a) Crash-stop → node fails once, gone forever

b) Crash-recovery → node crashes and restarts; stable storage survives

c) Byzantine → arbitrary behavior

Most practical combo = partially synchronous + crash-recovery

Safety v/s Liveness :-

Safety → nothing bad happens → must hold always, even under partition

Liveness → something good eventually happens → may have caveats

Safety violation = irreversible

Liveness violation = may still be satisfied later

Primary source: Kleppmann, M. (2017). Designing Data-Intensive Applications, Chapter 8: “The Trouble with Distributed Systems.” O'Reilly Media.
Recommended supplements: Kyle Kingsbury's Jepsen analyses (aphyr.com), Google Spanner paper, Phi Accrual failure detector paper, and the ZooKeeper fencing-token pattern.

Ask follow-up questions to drill into: why partial failures are nondeterministic, how TrueTime works under the hood, the difference between fencing tokens and leases, why logical clocks solve ordering when physical clocks cannot, or how the partially synchronous model makes consensus algorithms possible.

← Lesson 7: Transactions Lesson 9: Consistency and Consensus →