episode 2 · arc 1

Partial Failure and the
Fallacies of Distributed Computing

There are eight common assumptions people tend to make about networks when working with distributed systems. This episode goes through all eight, along with partial failure, the bigger problem underneath all of them.

What this episode covers

  • The GitHub outage from episode 1, revisited: the one assumption that broke, and why it's only the first of eight.
  • Partial failure, up close: what happens when one part of a system stops responding. Has it crashed, is it just slow, or is the network cutting it off? The rest of the system can't know for sure. But it still has to decide what to do.
  • Where the eight fallacies come from. Engineers at Sun Microsystems kept seeing the same wrong assumptions in people's designs, and over the 1990s they built a list of eight from them, for people building everyday applications.
  • Fallacies one to four, each shown in the URL shortener: the network is reliable, latency is zero, bandwidth is infinite, and the network is secure (with Operation Aurora and BeyondCorp).
  • Fallacies five to eight: topology doesn't change, there is one administrator (the 2016 Dyn DNS attack), transport cost is zero, and the network is homogeneous.
  • The misconception "our platform handles this now": service meshes, orchestrators and retries automate how we respond to the fallacies, but they don't make the assumptions true, and automatic retries can turn one slow service into a retry storm.

Exercises

The coding exercise is the one to prioritize. It puts a circuit breaker in your own hands, and after that you'll read that kind of code differently.

1 · Written problem

Your shortener's link-creation flow now calls a third-party fraud-check service before creating any new short link. That service runs in a different cloud, in a different country, and on a normal day the call takes about forty milliseconds. One afternoon, engineers notice that p99 latency for link creation has jumped to six seconds, and that some requests are timing out altogether. The team that owns the fraud-check service reports nothing wrong on their end.

  1. Name two of the eight fallacies this design assumed were true, and say specifically where each assumption is built into the design as described.
  2. The link service treats "no response within three hundred milliseconds" as "the fraud-check service is dead," and immediately creates the link locally without waiting further. Using this episode's partial-failure framework, explain how this can cause unintentional duplicate codes for the same URL.
  3. Propose one concrete change to the timeout, retry policy, or request flow that would reduce duplicate short codes, without simply "trusting the fraud-check service more."
Model answer

a) Strongest candidates:

  • Latency is zero. A three-hundred-millisecond timeout on a cross-cloud, cross-country call implicitly assumes the round trip is fast and stable. The p99 spike to six seconds shows the real latency profile of that link was never close to zero. It was just usually fast enough that nobody noticed the assumption.
  • The network is reliable. The design assumes every call to the fraud-check service either comes back promptly or the service is dead. There is no plan for a request or its reply being delayed or dropped somewhere along the way, which is what some requests timing out points to: packet loss and retransmission on a long cross-country path, not a service that is actually down.

Also acceptable:

  • There is one administrator. The fraud-check service belongs to a different team, in a different company, with different priorities and different visibility into your system. "Nothing wrong on our end" is exactly what you'd expect to hear from an administrator who doesn't share your incentives, your dashboards, or your incident channel.
  • Bandwidth is infinite. If the cross-region link is saturated by unrelated traffic, requests queue behind it and slow down without any error anywhere, which would look exactly like this.

b) Why duplicate codes appear for the same URL:

  • The link service guesses the fraud check is dead. After three hundred milliseconds it treats silence as death and creates the link locally. But silence can't tell "dead" from "just slow right now," the same three-way ambiguity from the replica example.
  • The client app makes the same guess, one layer up. Link creation now takes seconds, so the app hears nothing within its own timeout, assumes the server is dead, and retries the whole request.
  • Neither guess cancels the earlier attempt. The first request is still running, and may finish and create its link after the retry has started.
  • Every attempt mints its own code. Each attempt runs link creation from the top, unaware of the others, and each generates its own random short code.

This is not a bug in the code-generation logic itself. It is the same "unknown means dead" guess made at two layers, on top of link creation that isn't safe to repeat.

c) Any one reasonable proposal, for example:

  • Make link creation idempotent: check whether a code already exists for the URL and return it if so, otherwise create one, backed by a unique constraint on the URL. A retry then can't produce a second code for the same URL.
  • Raise the timeout to sit above the real observed p99 and add capped, jittered backoff plus a circuit breaker (this episode's coding exercise) instead of one hard cutoff that immediately assumes death.
  • Take the fraud check off the synchronous create path entirely: create the link in a provisional state, queue the fraud check, and confirm or revoke asynchronously, so a slow third party can no longer force a guess under time pressure.
  • Fail the whole request when the fraud check doesn't answer in time: return an error instead of guessing "dead" and creating the link anyway, so nothing is created until the check has actually answered. The cost is that link creation is unavailable while the fraud check is struggling.

2 · Coding exercise - slow isn't dead, stop guessing wrong on purpose

~20–30 minutes · JavaScript

The shortener's read replica sits on the other side of a network. replica.get(code) returns a promise. Usually it resolves to the long URL, but sometimes it rejects with TimeoutError, and from where you're standing that could mean the replica is merely slow or that it's genuinely unreachable. primary.get(code) always answers, but you'd rather not lean on it.

Implement a CircuitBreaker class and lookupCode(replica, primary, code, breaker, now) so that the lookup:

  1. prefers the replica while it's healthy,
  2. stops hammering the replica after a small number of consecutive timeouts,
  3. falls back to the primary while the breaker is open,
  4. gives the replica exactly one trial call after a cooldown, to see if it has recovered. A failed trial re-opens the breaker.

now() returns the current time in milliseconds (the tests control it, so nothing actually has to wait). Four tests run against whatever you type below. You can't see or edit the replica and primary themselves.

Hint, if you're stuck

The breaker only needs three pieces of state: a count of consecutive failures, the time it opened (or null), and whether the next call is the one trial call. Ask yourself which method should reset the failure count, and which shouldn't.

lookup.js

Click "Run tests" to check your implementation.

Reference solution
class CircuitBreaker {
  constructor(failThreshold = 2, cooldownMs = 1000) {
    this.failThreshold = failThreshold;
    this.cooldownMs = cooldownMs;
    this.consecutiveFailures = 0;
    this.openedAt = null;
    this.halfOpen = false;
  }

  allowCall(now) {
    if (this.openedAt === null) return true;
    if (now() - this.openedAt >= this.cooldownMs) {
      this.halfOpen = true; // the one trial call
      return true;
    }
    return false;
  }

  recordSuccess() {
    this.consecutiveFailures = 0;
    this.openedAt = null;
    this.halfOpen = false;
  }

  recordFailure(now) {
    this.consecutiveFailures++;
    if (this.halfOpen || this.consecutiveFailures >= this.failThreshold) {
      this.openedAt = now();
      this.halfOpen = false;
    }
  }
}

async function lookupCode(replica, primary, code, breaker, now = () => Date.now()) {
  if (breaker.allowCall(now)) {
    try {
      const result = await replica.get(code);
      breaker.recordSuccess();
      return result;
    } catch (err) {
      if (!(err instanceof TimeoutError)) throw err;
      breaker.recordFailure(now);
    }
  }
  return primary.get(code);
}

The breaker's whole job is to stop the caller from re-asking a question it already has enough evidence to stop asking, for a little while. That bounds the damage a wrong guess can do, which is the practical answer to "one flaky replica becomes a full outage." The half-open trial is itself still a guess, just a cheaper, rate-limited one.

Two things worth checking in your own version. Resetting the failure count inside allowCall instead of recordSuccess silently forgives failures the breaker never actually saw succeed. And nothing here is safe for concurrent callers sharing one breaker under real threads or processes: that is out of scope for this exercise.

3 · Quiz

  1. A payment service sends a "charge this card" request to a payment gateway and gets no reply before its timeout. What is the right way to think about the charge?
    Answer

    C.Silence isn't evidence. The request could have been lost, or applied with only the reply lost. Since the sender can't tell, the retry must be safe to repeat.

  2. A mobile app builds its profile screen with twelve API calls made one after another to a server on another continent. Each call takes about 90 ms round trip, and the server itself answers in 2 ms. The screen takes over a second to appear. What helps most?
    Answer

    A.The delay is in the distance, multiplied by the number of round trips, not in the servers or the payload size. Fewer round trips, or overlapping independent ones, is what moves the floor.

  3. A nightly job copies a 200 GB table between two data centers over a shared link that also carries production traffic. Nothing errors, but every night around 2 a.m. customer requests get noticeably slower. What is the most likely explanation and a sensible fix?
    Answer

    D.Bandwidth is finite, and a large transfer on a shared link takes capacity away from everything else, with no error to point at.

  4. An internal admin API has no authentication because it can only be reached from the office VPN. A contractor's laptop on the VPN gets compromised and the API is abused. What was the underlying mistake?
    Answer

    B.Where a request comes from says nothing about whether it should be trusted. Anything that can reach the service from inside the network inherits all of its access.

  5. A service reads the IP addresses of its cache nodes from a config file at startup. An autoscaler later replaces two of the nodes with new machines on new IPs, and the service keeps sending traffic to the old addresses. What should have been done?
    Answer

    A.Machines come and go, so a fixed list of addresses turns into a wrong map. The service has to ask where things are now, not remember where they were.

  6. Your checkout depends on a third-party address-validation API. The vendor ships a breaking change at 2 p.m. on a Friday with no notice, and checkout starts failing. What is the best lesson?
    Answer

    D.Someone outside your organization decides when their system changes. The protection is in how your system behaves when a dependency changes or fails, not in a promise from the dependency.

  7. A team moves a 5 TB dataset to another cloud vendor to use a cheaper compute service. The compute is cheaper, but the total monthly bill goes up. What did the plan miss?
    Answer

    B.Data has a cost to move as well as to store and process. Placing compute next to the data, or moving less of it, avoids paying that cost repeatedly.

  8. A file-upload feature works flawlessly in staging, where everything runs in one cloud network. In production, users on hotel Wi-Fi and older mobile networks see large uploads fail. What is the best reading of this?
    Answer

    C.Staging exercised one very specific network. Production requests cross many different networks, each with its own behaviour, so the software has to tolerate that diversity.

← all episodes