Skip to content
SparkTime, home
System Design From Zero to Interview-Ready
Lessons
1 · System Design Foundations
2 · APIs and Communication
3 · Capacity Estimation
4 · Core Building Blocks
5 · Databases
6 · Distributed Systems
7 · Reliability and Operations
8 · Architecture and Security
9 · Interview Method and Foundation Case Studies
10 · Real-World Case Studies
11 · Large-Scale Case Studies
12 · Interview Readiness
Lesson 18 of 45

Lesson 18 · Distributed Systems

Why Distributed Systems Are Hard: Partial Failure, Timeouts and Clocks

What changes when work is spread over many machines: partial failure, messages that are lost, delayed, duplicated or reordered, timeouts with unknown outcomes, process pauses, and clocks that disagree. Learn why a timeout does not tell you what happened, why wall clocks cannot order events, how logical clocks help, and the design habits that follow.

On this page

Key takeaways

  • In a distributed system, parts fail independently: some machines and links work while others do not, and you often cannot tell which.
  • A timeout has three possible meanings (it failed, it succeeded but the reply was lost, it is still running), so every retried operation must be safe to repeat.
  • Messages can be lost, delayed, duplicated and reordered; design consumers and protocols that tolerate all four.
  • Wall clocks on different machines disagree and can jump, so do not use them to order events across machines; use sequence numbers, versions or logical clocks.
  • A process can pause for seconds without knowing it, so time-based leases and locks need fencing.

Your service calls the payment provider to charge a customer ₹4,999. After two seconds, the call times out. What happened?

Maybe the request never reached the provider. Maybe it reached the provider, the card was charged, and the response was lost on the way back. Maybe the provider is still processing it and will charge the card in three seconds. From your side, all three look exactly the same: no response. If you retry, you might charge twice. If you do not, you might never charge at all.

On one machine, a function call either returns or throws. Across a network, there is a third outcome, "I don't know", and it changes everything. This article explains the handful of facts that make distributed systems hard, so that the techniques in the rest of Module 6 (consistency models, idempotency, sagas, locks) make sense as answers to real problems.

What you will learn

#
  • What makes a system "distributed", and why you already have one.
  • Partial failure, and why it is different from a crash on one machine.
  • What networks do to messages: loss, delay, duplication and reordering.
  • Why a timeout cannot tell you what happened, and how to choose timeouts.
  • Process pauses and why a node can be wrong about its own state.
  • Why clocks disagree, why wall-clock time cannot order events across machines, and how logical clocks help.
  • Design principles that follow, used throughout the rest of the course.

You already have a distributed system

#

A distributed system is a set of programs on different machines that cooperate by sending messages over a network. Pixly, as designed so far, already is one: clients, load balancers, a fleet of API servers, a cache cluster, a primary database with replicas, a queue and workers, object storage, a CDN, and third-party email and payment providers. Every arrow in the diagram is a network message that can go wrong.

Amazon's Builders' Library has an article on exactly this topic, arguing that the hard part of distributed programming is that every network interaction has many more ways to fail than a local call, and that engineers must handle each of them (Challenges with distributed systems).

Partial failure

#

On a single computer, failure tends to be total: if the machine crashes, everything on it stops together. In a distributed system, parts fail independently:

  • one of eighteen API servers runs out of memory;
  • the network link between two availability zones drops packets;
  • the cache is healthy but slow because one node is swapping;
  • the database primary is fine, but one replica is ten seconds behind;
  • the email provider accepts requests but delivers them an hour late.

This is partial failure, and it is the defining difficulty. The system is neither up nor down; it is partly working, in ways that are hard to observe and that change from moment to moment. Code written for "it works or it throws" is not ready for "it half works, sometimes, slowly".

What the network does to messages

#

A network between two machines can:

Behaviour

Example

Consequence if ignored

Lose a message

A packet dropped; a connection reset

Work never happens; the caller waits

Delay a message

Congestion, a slow route, a retransmission

Timeouts fire; stale data arrives late

Duplicate a message

A retry after a lost acknowledgement

The same work runs twice

Reorder messages

Two messages take different paths, or a retry overtakes

Updates applied in the wrong order

Partition the system

A switch fails; two groups of servers cannot reach each other

Each side may act alone (split brain, article 15)

TCP hides some of this inside one connection (it resends lost packets and keeps order), but not across connections, retries, queues or services. At the application level, assume all five happen.

There is also a deeper limit. Two parties communicating over a link that can lose messages cannot be certain they agree: every confirmation might itself be lost, and so might the confirmation of the confirmation. This is often explained as the "two generals" problem. Distributed systems therefore never achieve certainty by sending more messages; they use designs that stay correct when messages are lost or repeated.

Timeouts: the three outcomes

#

When a request gets no reply within a deadline, you know only that you did not hear back. The possibilities:

Code
1. The request was lost       -> nothing happened2. The work happened, but the reply was lost   -> it DID happen3. The work is still happening (slow server)   -> it MAY happen later

You cannot tell these apart from the caller's side. This one fact drives much of distributed-systems design:

  • Retrying is necessary (case 1 is common), but dangerous (cases 2 and 3 make retries duplicates). Retries are only safe for idempotent operations, which is why article 20 is about idempotency.
  • "Did my request succeed?" must be answerable later, for example by querying status with an idempotency key or request id.
  • The callee must tolerate duplicates, because callers will retry.

Choosing a timeout

#

Too short, and healthy-but-slow requests are cut off and retried, adding load exactly when the system is slow. Too long, and threads and connections are held hostage by a dead dependency (Little's Law, article 02).

A sound approach, described in the Amazon Builders' Library article on timeouts and retries, is to base the timeout on the downstream service's latency distribution: pick an acceptable rate of false timeouts (for example 0.1%) and set the timeout near the matching latency percentile (for example p99.9) (Timeouts, retries and backoff with jitter). Then check that your own overall deadline still meets your latency requirement: a request that calls three services in sequence cannot give each one a 2-second timeout and still promise a 1-second response.

Two more practical habits:

  • Propagate deadlines. If the user's request has 800 ms left, a downstream call should not wait 2 seconds. Pass the remaining budget along.
  • Separate connection and request timeouts. Failing to connect is usually quick to detect; a stuck response is not.

Process pauses: when a node is wrong about itself

#

A program can stop running for a while without knowing it:

  • a garbage-collection pause in a managed runtime such as the JVM;
  • the virtual machine being paused or migrated by the hypervisor;
  • the operating system swapping memory to disk;
  • a laptop lid closing on a developer's machine running a "production" script.

When the program resumes, it continues from where it was, believing almost no time has passed. If it held a lock that expired during the pause, or was the leader and another node was elected meanwhile, it will now act on authority it no longer has. Martin Kleppmann's analysis of distributed locking walks through exactly this scenario, with long garbage-collection pauses, and shows why locks for correctness need fencing tokens that let storage reject stale holders (How to do distributed locking). Article 22 builds on this.

The general lesson: a node cannot fully trust its own sense of time or status. Correctness must be enforced where the data lives, not only by the node that believes it is in charge.

Clocks: why time is not what it seems

#

Every machine has a clock, and the clocks disagree.

  • Skew and drift: hardware clocks run slightly fast or slow. Synchronisation protocols (such as NTP) correct them, but only to within some error, which depends on the network.
  • Jumps: when a clock is corrected, it can jump forward or backward. A timestamp taken later can be smaller than one taken earlier.
  • Two kinds of clock: a wall clock (time of day, suitable for showing users and for logs) and a monotonic clock (only moves forward, suitable for measuring durations and timeouts on one machine). Use the monotonic one for "how long did this take?".

Wall clocks cannot order events across machines

#

Suppose server B records "post created" and sends it to server A, which then records "comment added". If A's clock runs 50 ms behind B's, the comment can carry an earlier timestamp than the post it replies to. Sorting by timestamp shows the reply before the post. Worse, a database using last write wins by timestamp can silently keep the older of two writes.

Logical clocks

#

In 1978, Leslie Lamport showed how to order events consistently with cause and effect without synchronised clocks (Time, Clocks, and the Ordering of Events in a Distributed System). Each process keeps a counter:

  1. Increment it before each local event.
  2. Attach it to every message sent.
  3. On receiving a message, set the counter to max(local, received) + 1.

If event X could have caused event Y (X happened before Y on the same process, or X's message was received before Y), then X's counter is smaller than Y's. This Lamport clock gives an order that respects causality.

The program below simulates the post-and-comment example: wall clocks with 50 ms of skew put the comment first; Lamport clocks put the post first.

Java
// File: ClockDemo.javaimport java.util.ArrayList;import java.util.Comparator;import java.util.List;public class ClockDemo {    record Event(String what, String server, long wallClockMs, long lamport) {}    public static void main(String[] args) {        long skewOfA = -50;          // server A's wall clock runs 50 ms behind true time        long lamportA = 0;        long lamportB = 0;        List<Event> events = new ArrayList<>();        // True time 1000 ms: server B creates a post.        lamportB++;        events.add(new Event("post created", "B", 1000, lamportB));        long stampOnMessage = lamportB;            // B sends the post to A, with its Lamport time        // True time 1003 ms: A receives it. Rule: max(local, received) + 1.        lamportA = Math.max(lamportA, stampOnMessage) + 1;        // True time 1004 ms: A records a comment on that post.        lamportA++;        events.add(new Event("comment added", "A", 1004 + skewOfA, lamportA));        List<Event> byWallClock = new ArrayList<>(events);        byWallClock.sort(Comparator.comparingLong(Event::wallClockMs));        System.out.println("Sorted by wall clock:");        for (Event e : byWallClock) {            System.out.println("  " + e.wallClockMs() + " ms  " + e.what() + " (server " + e.server() + ")");        }        List<Event> byLamport = new ArrayList<>(events);        byLamport.sort(Comparator.comparingLong(Event::lamport));        System.out.println("Sorted by Lamport clock:");        for (Event e : byLamport) {            System.out.println("  L=" + e.lamport() + "  " + e.what() + " (server " + e.server() + ")");        }    }}
Code
Sorted by wall clock:  954 ms  comment added (server A)  1000 ms  post created (server B)Sorted by Lamport clock:  L=1  post created (server B)  L=3  comment added (server A)

Lamport clocks have a limit: if two events are concurrent (neither could have caused the other), their counters still compare, so the order between them is arbitrary. Vector clocks (a counter per process) can detect that two updates are concurrent, which is useful for spotting conflicts. Amazon's Dynamo paper describes using vector clocks to detect conflicting versions of the same object (Dynamo).

When you need real time

#

Some systems do need globally meaningful timestamps. Google Spanner, for example, uses its TrueTime infrastructure, which exposes clock uncertainty explicitly and waits it out to give transactions a consistent global order (Spanner: TrueTime and external consistency). The lesson for ordinary designs is not to copy that, but to notice what it took: special hardware and careful protocol design. Without such infrastructure, do not rely on timestamps from different machines to decide which write wins.

Practical rules for time

#

Need

Use

Measure how long something took on one machine

Monotonic clock

Show a time to a user, write a log line

Wall clock (synchronised, in UTC)

Order events of one entity

A sequence number assigned in one place (a database row version, a per-conversation counter, a partition offset)

Order events across machines consistently with causality

Logical clocks, or pass versions along with messages

Decide which of two concurrent writes wins

An explicit rule (version check, merge, or reject), not "latest timestamp"

Put together: what goes wrong in practice

#

Situation

What can happen

Defence

Client retries a timed-out payment

Customer charged twice

Idempotency key (article 20)

Worker crashes after sending an email, before acknowledging

Email sent twice

Idempotent consumer, deduplication

Two updates to one record arrive out of order

Older value overwrites newer

Versions; conditional updates (article 14)

Cache invalidation message lost

Stale data until TTL

TTLs on every entry (article 09)

Network partition between primary and failover manager

Two primaries (split brain)

Fencing; majority-based election (article 22)

Leader pauses for 20 seconds, resumes

Two nodes act as leader

Leases plus fencing tokens (article 22)

Timestamps from skewed clocks decide conflicts

Newer write silently lost

Versions or explicit conflict handling

One slow dependency in a synchronous chain

Threads exhausted, cascading failure

Timeouts, circuit breakers, bulkheads (article 23)

Design principles that follow

#

These habits appear in every remaining article. Learn them as a list:

  1. Assume every remote call can fail, hang, or succeed without telling you. Give it a timeout and a plan.
  2. Make operations idempotent so retries and redeliveries are safe.
  3. Prefer at-least-once delivery plus deduplication over hoping for exactly-once.
  4. Do not order events by wall-clock time across machines. Use sequence numbers and versions.
  5. Enforce correctness where the data lives (constraints, conditional writes, fencing), not only in the node that thinks it is in charge.
  6. Keep the synchronous critical path short. Every hop is another way to fail.
  7. Design for partial failure: degrade a feature instead of failing everything.
  8. Make the system observable, because partial failures are invisible without good signals (article 25).
  9. Prefer simple designs. Each extra component and protocol multiplies the failure combinations.

Trade-offs

#

Decision

You gain

You pay

Short timeouts

Fast failure, protected resources

More false timeouts and retries

Long timeouts

Fewer false timeouts

Resources held by dead dependencies

Retrying

Recovery from transient loss

Duplicates; extra load during incidents

Logical clocks or versions

Correct ordering and conflict detection

More metadata and logic

Synchronous coordination

Simple reasoning

Availability and latency coupled across machines

Common mistakes

#
  • Treating a timeout as "it failed" and blindly retrying a non-idempotent operation.
  • No timeouts, or one fixed timeout for everything.
  • Using System.currentTimeMillis() (or any wall clock) differences across machines for ordering or conflict resolution.
  • Assuming messages arrive once and in order.
  • Assuming a lock or a leadership role is still held because no error was seen.
  • Testing only the happy path; never injecting delays, drops or duplicates.

In an interview

#
  • When a design calls another service, say what happens on timeout, and whether retries are safe.
  • When ordering matters, say where the sequence number comes from.
  • When something is replicated or partitioned, mention partial failure: "if the link between zones drops, this side will..."
  • Use the vocabulary precisely: partial failure, idempotent, at-least-once, clock skew, fencing.

Exercises

#

Exercise 1

The three outcomes

#

A client calls POST /v1/orders and the request times out. Then a client calls GET /v1/orders/77 and it times out. For each, list what might have happened on the server, and what the client should do next.

Exercise 2: Choose a timeout

#

A recommendation service your API calls has these latencies: p50 40 ms, p99 300 ms, p99.9 900 ms, p99.99 3 seconds. Your API must respond within 1 second at p99, and it also calls a 200 ms profile service in sequence. What timeout would you set on the recommendation call, and what does your API do when it fires?

Exercise 3: Last write wins

#

Two servers handle edits to the same document. Server X's clock is 2 seconds ahead. A user saves version 1 through X at true time 10:00:00 (stamped 10:00:02), then saves version 2 through Y at true time 10:00:01 (stamped 10:00:01). The database keeps the write with the larger timestamp. What does the user see, and how would you fix the design?

Exercise 4: Name the assumption

#

For each bug, name the false assumption about distributed systems behind it:

  1. A job scheduler on two servers sometimes runs a job twice after a network blip.
  2. A notification worker sends duplicates after deployments.
  3. Analytics show some users "commenting before the post existed".
  4. During a dependency outage, the API's thread pool fills and the whole service stops responding.
Show solutionHide solution

POST /v1/orders timed out. The order may not exist (request lost), may exist (created, response lost), or may be about to exist (still processing). The client should retry with the same idempotency key, so the server returns the original order if it was created, or creates it once if not. If the client has no idempotency key, it should look the order up (for example by a client-generated reference) before trying again, rather than blindly re-posting, which risks a duplicate order.

GET /v1/orders/77 timed out. Reading changes nothing, so any of the possibilities is harmless. The client can simply retry (with backoff), because GET is idempotent.

Exercise 2

Show solutionHide solution

The total budget is 1 second at p99. The profile call takes about 200 ms, leaving roughly 800 ms, minus your own processing and network overhead, say 150 ms: about 600 ms is the most the recommendation call can have. Its p99.9 is 900 ms, so a 600 ms timeout will cut off somewhat more than 0.1% of calls (somewhere between p99 and p99.9), which is acceptable if you have a fallback.

When it fires: degrade gracefully instead of failing the request. Return the page with generic or cached recommendations (or none), log and count the timeout, and do not retry inside the request (there is no budget). If timeouts rise sharply, a circuit breaker can stop calling the service for a while (article 23).

Better still, make the two calls in parallel if they are independent: the budget for each becomes most of the second.

Exercise 3

Show solutionHide solution

Version 1 carries the timestamp 10:00:02 and version 2 carries 10:00:01. The database keeps the "later" timestamp, which is version 1. The user's most recent save (version 2) is silently discarded; they reload and see their older text. No error was raised anywhere.

Fixes:

  • Version numbers instead of timestamps: each save sends the version it was based on; the database accepts it only if the version matches, then increments it (optimistic concurrency, article 14). Version 2 is based on version 1 and wins; a truly concurrent edit gets a conflict to resolve.
  • A single authority for ordering: route all writes for one document to one place (one database primary, or one partition) and let it assign a sequence number.
  • Keep clocks synchronised anyway (for logs), but never let them decide which data wins.

Exercise 4

Show solutionHide solution
  1. "A node always knows whether it is the leader / holds the lock." After the blip, both servers believed they were in charge. Fix with proper leader election and fencing, plus idempotent jobs (articles 22 and 37).
  2. "Each message is delivered exactly once." Deployments kill workers mid-processing; messages are redelivered. Fix with idempotent consumers and graceful shutdown (articles 11 and 20).
  3. "Clocks on different machines agree." Events were ordered by skewed wall clocks. Fix with logical ordering or causal metadata.
  4. "The network is reliable and fast; latency is zero." Calls without timeouts or isolation tied up every thread. Fix with timeouts, bulkheads and circuit breakers (article 23).

Where to go next

#

Sources & further reading

  1. aws.amazon.com
  2. aws.amazon.com
  3. lamport.azurewebsites.net
  4. allthingsdistributed.com
  5. martin.kleppmann.com
  6. docs.cloud.google.com
  • Guide · Technology

    Start Here: Your Roadmap from Zero to Job-Ready DSA

  • Guide · Technology

    Heaps and Priority Queues

  • Guide · Career

    A LinkedIn Profile That Gets Recruiter Messages