Challenges of Distributed Systems: Partial Failure and Concurrency

Imagine a bank transfer that debits your account. Then a network cable is unplugged before the credit arrives. Was the transfer "in progress," "failed," or "half done"? A single machine either finishes an instruction or it does not run at all. But a distributed system can fail in the middle. Some parts of the computation get done and others do not. This half-finished state is called partial failure. It has no equivalent on a single computer. It forces an entirely different way of thinking about correctness. Distributed systems matter because almost every real system you use is built this way. Search engines, banking, cloud storage, multiplayer games, ride-sharing dispatch — all rely on many machines cooperating over a network. The moment you split a computation across machines, you inherit four intertwined problems. Parts of the system can fail independently of others. Multiple machines act concurrently with no natural ordering of their actions. There is no shared global clock to timestamp events consistently. Messages between machines can be delayed by an unbounded and unpredictable amount, or lost outright. These are not implementation bugs to be patched away. They are fundamental properties of physics and engineering. Any distributed algorithm must be designed around them. The big picture this article delivers is a precise vocabulary and mental model. It explains why distributed systems are hard, before diving into specific fixes. Later material on Remote Procedure Calls shows how these challenges surface. This happens when a client tries to call a function on a remote machine. Material on Clocks and Time in Distributed Systems shows how to build usable notions of "before" and "after." This works even without a shared clock. Material on Fault Tolerance and Failure Models shows how to formally classify the many ways a component can misbehave. It also shows how to build systems that tolerate them. This article treats four core difficulties as a single connected phenomenon. These are partial failure, concurrency, the absence of global time, and unbounded delay. They are not four unrelated facts. Understanding how they interact is the foundation for every distributed algorithm you will study afterward. Consensus, replication, distributed transactions, and failure detection all exist specifically to tame these four problems.

Key takeaways
  • A distributed system can be in a partial failure state: some components have completed an action, others have not, and no single observer can immediately tell which is which.
  • Concurrency means multiple machines execute independently with no natural total order on their actions, so interleavings must be reasoned about explicitly.
  • There is no global clock: local clocks drift, so timestamps from different machines cannot be compared naively to decide "happened before."
  • Unbounded message delay means a lack of response never definitively means failure — the message or reply might simply be very slow.
  • These four problems combine to make the fundamental question "is machine X alive?" undecidable in general in an asynchronous network — this is the core intuition behind the FLP impossibility result.
  • Real systems cope not by eliminating these problems but by using timeouts, retries, idempotence, replication, and consensus protocols to build usable guarantees on top of an inherently unreliable substrate.
The core insight

In a distributed system, failure is not a Boolean event but an ambiguous, partially-observed state that every component must reason about probabilistically, not certainly.

The idea

A single-machine program executes instructions atomically with respect to failure: either the process runs to completion, or a crash aborts everything before it, and the machine's own memory always reflects one consistent state. A distributed system removes this guarantee. Suppose machine A sends a message to machine B and machine C, intending both to apply an update. If A crashes after B receives the message but before C does, the system is left in a state where B has applied the update and C has not — and neither A nor any external observer can immediately know this. This is partial failure: a failure mode in which the system is neither fully "up" nor fully "down," but stuck in an inconsistent, partially-completed configuration. Partial failure is compounded by concurrency. On a single machine with one thread, actions happen in one sequential order, and there is exactly one history to reason about. In a distributed system, every machine runs its own program independently, at its own pace, and these executions interleave in ways no single machine controls or fully observes. Two machines might both try to update the same replicated record at the "same time" — but "same time" is itself a slippery notion, because there is no global clock shared across machines. Each machine has its own local hardware clock, these clocks drift apart at different rates (clock skew), and network delay means a message timestamped "10:00:00" at the sender might not be observed until well after 10:00:00 at the receiver. Comparing timestamps from different machines to decide which event happened first is unreliable, sometimes actively misleading. The final piece is unbounded message delay. In an asynchronous network model — the honest model for the real Internet — there is no upper bound on how long a message can take to arrive, and there is no way to distinguish "the message is still in transit" from "the message was lost" from "the receiver crashed and will never reply." This is formalized as the impossibility of reliably detecting failure using only timeouts: a timeout firing means either the remote machine crashed, or the network is slow, or the reply is on its way and just hasn't arrived yet — from the observer's point of view these three cases are indistinguishable in the general asynchronous model. This single fact — that you cannot reliably tell "dead" from "just slow" — is the seed from which nearly every hard problem in distributed systems grows, including the celebrated FLP impossibility result (Fischer, Lynch, and Paterson, 1985), which proves that no deterministic consensus algorithm can guarantee agreement in a purely asynchronous system if even one process may crash.

How it works

Rather than a single algorithm, this article's "mechanism" is a reasoning framework: a systematic way to enumerate what can go wrong when two machines try to coordinate, and the standard defensive techniques used in response. Consider the canonical example of a client calling a remote server to increment a bank balance.

Step 1 — enumerate the failure points. Every remote operation decomposes into: (a) the request message travels client → server, (b) the server executes the operation, (c) the response message travels server → client. Failure or unbounded delay can strike at any of these three points, and from the client's perspective, a lack of response is ambiguous across all three.

Step 2 — recognize the ambiguity explicitly. If the client sends "increment balance by $10" and receives no reply within its timeout window, it cannot distinguish:

  • the request was lost before reaching the server (operation never happened),
  • the request arrived and was applied, but the response was lost (operation happened once),
  • the request is still queued or the server is merely slow (operation may still happen).

Step 3 — choose a policy under uncertainty. Systems must pick an explicit strategy rather than pretend the ambiguity does not exist. The dominant strategy is timeout + retry, combined with making the operation idempotent (safe to apply more than once) so that retries cannot cause a double-increment. This requires the server to track a unique request identifier and deduplicate.

Step 4 — never let local success imply global success. A machine can only be certain about its own local state. It must not assume a message it sent was received, nor that a peer it has not heard from is dead — it can only maintain a belief, refined over time by heuristics like accrual failure detectors that produce a suspicion level rather than a binary answer. Below is pseudocode for the standard client-side pattern that copes with these ambiguities — an idempotent retry protocol — followed by the server-side deduplication logic it depends on. ``` // ---- CLIENT SIDE ---- function reliable_call(server, operation, args, max_retries, timeout): request_id = generate_unique_id() // e.g., (client_id, monotonic_counter) attempt = 0 while attempt < max_retries: send_message(server, REQUEST, request_id, operation, args) response = wait_for_reply(timeout) if response is not TIMEOUT: return response.result // success: got a definitive answer attempt = attempt + 1 timeout = timeout 2 // exponential backoff return FAILURE_UNKNOWN // give up: outcome is still ambiguous! // ---- SERVER SIDE ---- seen_requests = empty_map() // request_id -> cached result function handle_request(request_id, operation, args): if request_id in seen_requests: return seen_requests[request_id] // duplicate: return cached result, // do NOT re-execute the operation result = execute(operation, args) // executed exactly once seen_requests[request_id] = result return result ``` Two properties make this pseudocode correct under partial failure. First, `request_id` gives the server a way to detect a retried message is* a retry rather than a fresh, second operation — this converts "at least once delivery" (the network's actual guarantee) into "exactly once execution" (what the application needs). Second, `FAILURE_UNKNOWN` is returned honestly rather than the client fabricating a definitive answer: after exhausting retries, the client genuinely does not know whether the operation happened, and the pseudocode does not pretend otherwise. This honesty is the crux of correct distributed programming — the interface exposes the ambiguity instead of hiding it.

Figure
Ambiguity of a lost response: three indistinguishable causes of client timeout
Client sees only: no reply before timeout Client Server Case A request X lost in transit Case B request arrives, executed once reply lost Case C request in flight, still processing... reply arrives LATE, after timeout fired client timeout fires here — identical observation in A, B, and C

Complexity & correctness

Time and message complexity. The reliable-call protocol above sends at most `max_retries` request messages and receives at most `max_retries` reply messages, so its worst-case message complexity is ``` O(max_retries) ``` per logical operation. Because delays are unbounded in the asynchronous model, no finite bound on wall-clock latency can be guaranteed — the protocol trades a bounded number of messages for an unbounded possible wall-clock time to completion (each retry doubles the timeout under exponential backoff, so total elapsed time before giving up is `O(timeout_0 · 2^max_retries)` in the worst case, though typically far smaller). The server's deduplication table (`seen_requests`) costs `O(1)` amortized time per lookup/insert using a hash map, but its space grows with the number of distinct in-flight clients unless entries are garbage-collected after an acknowledged window — a real engineering tradeoff between memory and correctness.

Correctness argument — the deduplication invariant. The key correctness property to prove is: > Invariant: for any `request_id`, `execute(operation, args)` is called at most once on the server, regardless of how many times the client retransmits the request. Proof sketch. The server checks `request_id in seen_requests` before calling `execute`. The first time a given `request_id` arrives, the check fails (map is empty for that key), so `execute` runs and the result is immediately cached in the same step. Every subsequent arrival of a message with the same `request_id` — whether a genuine retry or a duplicate delivered by the unreliable network — finds the key already present and returns the cached result without calling `execute` again. Assuming the server processes each incoming message atomically with respect to the `seen_requests` map (no two handlers for the same key interleave), the invariant holds by induction on the sequence of arrivals for that key. ∎ This shows the server-side exactly-once property is achievable — but it rests on an assumption we must be honest about: request handling must be atomic with respect to the deduplication check, which itself becomes a distributed coordination problem if the server is replicated across multiple machines (a preview of what Fault Tolerance and Failure Models addresses in depth).

The honest limitation — why FLP matters. No amount of clever protocol design removes the fundamental ambiguity of a lost message in an asynchronous network: the client can never be brought to 100% certainty about whether its last, un-acknowledged retry executed, if the server crashes permanently right after execution but before sending the acknowledgment. The pseudocode's `FAILURE_UNKNOWN` return value is not a design shortcoming — it is the mathematically correct answer, because a stronger claim would be a lie the protocol cannot back up. This is precisely the content of the FLP impossibility theorem: in a purely asynchronous system where even a single process may crash, no deterministic algorithm can guarantee that all correct processes reach agreement in bounded time, because an adversary can always delay a critical message just long enough to keep the system undecided. Real systems escape this theoretical wall not by disproving it, but by adding assumptions FLP excludes — partial synchrony (message delays are eventually bounded, even if the bound is unknown), randomization, or failure detectors with weak but useful guarantees.

Worked example

Trace a concrete scenario: a client calls `reliable_call` to withdraw $50 from an account, with `max_retries = 3` and an initial `timeout = 100 ms`.

StepClient actionNetwork / Server eventClient observationServer state after
1Sends `REQUEST(id=77, withdraw, 50)`, waits 100msMessage in transit, delayed by congestionTimeout fires at 100ms (no reply)(unaware) `seen_requests` unchanged
2Retries: sends `REQUEST(id=77, withdraw, 50)` again, waits 200msFirst message finally arrives late; server executes withdrawal, caches result under id=77, replies. Second (retry) message also arrives shortly after, server sees id=77 already in map, returns cached result (no re-execution)Client receives a reply (from whichever message's response arrives first) within 200ms window`seen_requests[77] = {balance: -50 applied}`, withdrawal executed exactly once
3Client processes reply, returns result to application—Application sees successful withdrawal of $50Balance debited once, correctly

Now trace the failure case that motivates `FAILURE_UNKNOWN`: suppose in Step 2 the server executes the withdrawal and writes the result to `seen_requests`, but then the server process crashes before sending the reply, and never restarts (the deduplication cache is lost with it). The client retries up to `max_retries = 3`, receives no reply each time, and returns `FAILURE_UNKNOWN`. The withdrawal did happen — the money is gone — but the client's honest, provably-correct answer is "I don't know." This is not a flaw in the pseudocode; it is the fundamental, irreducible ambiguity of partial failure made visible rather than papered over. A real banking system resolves this residual ambiguity at a layer above the RPC call itself — for example, by having the client later query "what is my current balance?" (an idempotent read) or "did request id=77 execute?" against a durable, replicated log — techniques developed further under Fault Tolerance and Failure Models.

Common pitfalls & traps

PitfallWhy it happensConsequence
Treating timeout as proof of failureConfusing "no reply yet" with "definitely crashed"Duplicate operations, double charges, split-brain
Comparing raw wall-clock timestamps across machines to order eventsAssuming a shared global clock existsIncorrect causal ordering, "future" events appearing before their cause
Retrying non-idempotent operationsForgetting that "at least once" delivery is the network's real guaranteeDouble withdrawal, duplicate emails, corrupted counters
Assuming a crashed process "knows" it crashed and can clean upReasoning from the single-machine mental modelStale locks, orphaned resources, resources never released
Ignoring message reorderingAssuming FIFO delivery when the network provides noneLater-sent update applied before an earlier one, inconsistent final state
Designing without a plan for "response never comes"Optimism bias in prototype codeProduction system hangs indefinitely on a single slow/dead peer

Pioneers & history

The formal understanding that asynchronous distributed agreement has provable limits emerged from theoretical computer science in the 1980s, but the practical recognition that distributed computing differs fundamentally from single-machine computing grew alongside the deployment of early networked systems — timesharing systems, ARPANET, and later distributed file systems and databases — where engineers repeatedly rediscovered that "the network is not reliable" and "you cannot tell dead from slow."

Origins · 1 minLeslie Lamport and how this was discovered
LL
Leslie Lamport
1941–present · Pioneer of distributed systems theory whose 1978 paper on ordering events formalized why no global clock exists across machines, and whose later work underpins nearly all modern consensus and replication protocols

NL
Nancy Lynch
1948–present · Co-author of the 1985 FLP impossibility proof (with Michael Fischer and Michael Paterson), which rigorously established that deterministic consensus cannot be guaranteed in a fully asynchronous system with even one faulty process

History
1978
— Leslie Lamport's "Time, Clocks, and the Ordering of Events" formalizes the absence of global time and introduces logical clocks.
1985
— Fischer, Lynch, and Paterson publish the FLP impossibility result for asynchronous consensus.
1988
— Birman and Joseph formalize virtual synchrony, an early practical framework for group communication under partial failure.
1996
— Chandra and Toueg formalize unreliable failure detectors, giving a rigorous middle ground between "impossible" and "trivially solvable."
2000
s — Large-scale internet systems (Google, Amazon) popularize idempotent APIs, exponential backoff, and quorum-based replication as standard industrial practice against these exact problems.

Where this matters

Every cloud platform, distributed database, and microservice architecture is built directly on top of these four challenges. Payment systems use idempotency keys — precisely the `request_id` pattern above — to guarantee a retried "charge card" API call never double-charges a customer. Distributed databases like those built on Paxos or Raft exist specifically to reach agreement despite partial failure, building directly on the theoretical limits FLP describes. Content delivery networks and load balancers rely on failure detectors (heartbeats with adaptive timeouts) that acknowledge they can only produce suspicion, not certainty, about a remote server's liveness. Distributed logging and tracing infrastructure (used to debug microservices) exists precisely because there is no shared clock — teams must reconstruct causal order from logical timestamps rather than trusting wall-clock time across machines. Every retry-with-backoff library, every "at least once" versus "exactly once" delivery guarantee documented in a message queue's API, and every discussion of "eventual consistency" in a NoSQL database is a direct, practical descendant of the four challenges introduced here — and the material on Remote Procedure Calls, Clocks and Time in Distributed Systems, and Fault Tolerance and Failure Models each tackle one of these challenges in depth.

Ask the tutor: How do logical clocks and vector clocks let us recover a usable notion of "before" and "after" without a shared global clock?

What exactly does "partial failure" mean, in one sentence?

Partial failure means a distributed operation can be interrupted so that some machines have applied its effects while others have not, and no participant can immediately and certainly determine which state the system is in. This differs sharply from a single machine, where a crash either happens before an instruction completes or after, never "during" in an ambiguous sense visible to other components. The ambiguity is the defining feature: it is not merely that failure can happen, but that the system's own knowledge of whether it happened becomes uncertain and must be actively resolved through protocols like acknowledgments, retries, and idempotent operations, rather than assumed away.

Is partial failure just a rare edge case, or does it matter for everyday system design?

It matters constantly, not rarely. Any system with more than one machine — which includes virtually every production web service, database, or cloud application today — routes every cross-machine call through a network that can delay, reorder, duplicate, or drop messages at any time. Engineers who ignore partial failure end up with systems that work in testing (low latency, no crashes) but corrupt data or hang indefinitely in production under real network conditions. This is why idempotency keys, retry policies, circuit breakers, and timeout tuning are standard, load-bearing parts of production system design rather than optional extras.

Why can't we just use a really accurate synchronized clock to fix the "no global clock" problem?

Even highly accurate clocks (like GPS-synchronized clocks used in some data centers) have nonzero drift, nonzero synchronization uncertainty, and are still subject to the fact that the message carrying a timestamp still experiences unbounded network delay before it is observed. Google's Spanner database uses tightly synchronized atomic clocks with an explicit uncertainty bound (`TrueTime`) precisely because it cannot assume perfect synchronization — it engineers around the residual uncertainty rather than pretending it is zero. For most systems without such specialized hardware, logical clocks (Lamport clocks, vector clocks) that capture causal "happened-before" relationships are far more robust than trusting wall-clock timestamps.

Does making an operation idempotent completely solve the retry problem?

Idempotence solves the correctness problem of retries — applying the same operation twice has the same effect as applying it once — but it does not solve the uncertainty problem. The client can still end up in the `FAILURE_UNKNOWN` state if every retry times out and it must give up, even though idempotence guarantees that if the operation did eventually execute, it executed correctly exactly once from the system's point of view. Idempotence is necessary for safe retries but does not, by itself, give the client certainty about the outcome; that requires an additional mechanism such as a durable, queryable log of completed operations.

How does concurrency interact with partial failure to make things worse?

Partial failure alone means one machine's action might not be reflected elsewhere; concurrency means many machines are acting independently at the same time, so there are combinatorially many possible interleavings of successes, failures, and message arrivals to reason about. For example, two clients might concurrently retry the same idempotent operation while a third machine is simultaneously replicating the result to a backup, and the backup itself might crash mid-replication. Reasoning about correctness requires showing the system behaves correctly for every possible interleaving and every possible subset of failures, not just the "nothing goes wrong" case — this is why formal invariants and proof sketches, rather than testing alone, are essential tools in distributed systems design.

What is the practical relationship between this article and consensus algorithms like Paxos or Raft?

Consensus algorithms exist specifically to let a group of machines agree on a single value (such as "who is the leader" or "what is the next entry in a replicated log") despite partial failures, concurrency, and unbounded delay. The FLP impossibility result shows this cannot be done with a fully deterministic algorithm in a purely asynchronous model with even one crash failure, so Paxos and Raft both make additional practical assumptions — for instance, that message delays are eventually bounded in practice even if not provably so, or using randomized timeouts to break symmetry. Understanding partial failure and the timeout ambiguity described here is the essential prerequisite for understanding why these algorithms are structured the way they are.

Why does the article say the client should return `FAILURE_UNKNOWN` instead of just retrying forever?

Retrying forever assumes the server or network will eventually recover, which may be true in many systems but is not guaranteed, and an application often needs to make progress (report an error, alert a human, try an alternate server) rather than block indefinitely. Returning `FAILURE_UNKNOWN` after a bounded number of retries is the honest acknowledgment that the client has exhausted its evidence-gathering ability and must hand the decision to a higher layer — for example, a human support process, a durable audit log lookup, or a fallback service. This bounded, honest failure mode is far safer than either blocking forever or falsely claiming success or failure without proof.

Before this

Where this leads