Timeouts, Retries, and Backoff

LESSON

Networking and Failure Models

003 30 min intermediate

Timeouts, Retries, and Backoff

By the end of this lesson, you will be able to...

  • Explain what a timeout proves, and what it does not prove.

  • Choose retry behavior from operation semantics, idempotency, deadlines, and overload signals.

  • Review a timeout/retry policy for failure amplification risk.

Idea in one sentence: Recovery logic is part of the failure model: it can contain uncertainty, or it can multiply the failure.

Core Insight

The learning platform has a familiar path now. A learner finishes lesson 043. The browser calls the gateway. The gateway calls the progress service. The progress service writes completion and publishes a progress event.

Most days, the request is boring. That is good.

During a live cohort launch, the progress service gets slow. The gateway waits 300 ms, receives no response, and times out.

The naive idea is:

If a request times out, retry it.

Sometimes that is exactly right. A retry can hide a temporary packet loss, a restarted instance, or a brief overload response. But a retry is not free. It can repeat a side effect, spend the user's remaining deadline, and add load to the dependency that is already struggling.

A timeout is not a verdict about the server. It is a local decision by the caller: "I stopped waiting." The server may have done nothing. It may have committed the write. It may still be working. It may be overloaded and about to fail more requests.

Timeouts, retries, backoff, jitter, and retry budgets are one policy bundle for acting under that uncertainty. They should be reviewed together.

The Small Incident

Here is the incident in small form:

learner clicks Complete lesson
  -> browser sends request to gateway
  -> gateway calls progress service
  -> progress service is slow
  -> gateway times out after 300 ms

The gateway has an 800 ms user deadline for the whole page interaction. It has already spent time on authentication, metadata, and rendering. Only 350 ms remain when it calls the progress service.

The progress service normally responds in 80 ms. During the launch, p95 latency rises to 500 ms. Some requests still succeed. Some queue. Some hit overload protection.

Now the gateway must decide:

Plain meaning:

A timeout is a caller-side stop rule.

In this scenario:

The gateway did not receive a useful answer before 300 ms. That is all it knows for sure.

Technical name:

This is a local observation under partial failure. The caller observes missing response before deadline, not global operation outcome.

Timeout Means "I Stopped Waiting"

A timeout answers one question:

Did this caller receive a useful response before its deadline?

It does not answer these questions:

Did the server receive the request?
Did the server commit the write?
Did the response get lost?
Is the server slow, crashed, overloaded, or partitioned away?

That distinction matters most for side effects.

For a read, uncertainty is often manageable. If GET /lessons/043 times out, the gateway may retry another replica or serve cached content.

For a write, uncertainty can change reality. If POST /complete-lesson times out after the progress service commits the write, a blind retry may create duplicate events, duplicate certificate checks, or confusing audit records.

The safer mental model is:

timeout = caller stopped waiting
retry = caller chooses to create another attempt
idempotency = receiver can recognize repeated intent
deadline = upper bound on useful work

Timeout values should come from the surrounding user or workflow budget. A user-facing path may use a short timeout because a late answer is no longer useful. A background reconciliation job may wait longer because it is not holding up a person.

The trade-off is responsiveness versus ambiguity. Short timeouts keep callers from hanging, but they increase the number of ambiguous outcomes. Long timeouts reduce ambiguity for slow successful work, but they tie up resources and delay fallback.

Check: The gateway times out after 300 ms. Which statement is safe?

Think first, then reveal.

Answer: "The gateway stopped waiting before it received a useful response." It is not safe to say "the progress service did not commit the completion."

A Worked Request Trace

Follow one completion request with the important states visible.

Input:
  POST /complete-lesson
  learner_id=7
  lesson_id=043
  idempotency_key=req-43-a
  gateway_deadline_remaining=350 ms

Transition:
  gateway sends attempt 1 to progress service
  progress service receives request after 40 ms
  progress service queues behind slow writes

Intermediate state:
  gateway reaches 300 ms timeout
  progress service commits at 330 ms
  response reaches gateway after gateway stopped waiting

Output or decision:
  gateway observes timeout
  progress service has already committed completion

Naive failure contrast:
  blind retry without idempotency may create duplicate side effects
  retry with the same idempotency key can return the existing result

Here is the same trace as a timeline:

Time Gateway state Progress service state What is known
0 ms sends attempt 1 no request yet gateway knows it sent bytes
40 ms waits receives request service may commit later
300 ms timeout fires still processing gateway outcome is ambiguous
330 ms considering retry commits completion service outcome is success
360 ms old response arrives too late response sent gateway may ignore it

The weird part is the split between observation and reality. The gateway's observation is timeout. The service's reality is success. A distributed system can have both at the same time because components learn facts at different moments.

So far:

Retry Safety Depends On Semantics

A retry is safe only when repeating the operation is acceptable.

That sentence sounds simple, but it requires application knowledge. Transport status alone is not enough. HTTP status alone is not enough. The retry policy needs to know what the operation means.

Compare three operations:

Operation Retry risk Safer policy
GET /lesson/043 low, if the read has no side effect retry while deadline remains
POST /complete-lesson medium, because it changes learner state retry only with idempotency key or deduplication
POST /send-certificate-email high, because duplicates are visible to users avoid blind retry; deduplicate by stable request id

For the completion write, a useful receiver rule might be:

if idempotency_key has already committed:
  return the existing completion result
else:
  commit completion and remember idempotency_key

The retry policy then has something real to rely on.

def should_retry(kind, status_code, idempotency_key, deadline_remaining_ms):
    transient = status_code in {429, 500, 502, 503, 504}
    if deadline_remaining_ms < 100:
        return False
    if kind == "read":
        return transient
    if kind == "write" and idempotency_key:
        return transient
    return False

This small function hides a big design point. The client library can implement retries. The application must provide the operation kind, the idempotency rule, and the deadline.

The trade-off is resilience versus correctness risk. Retries can improve success for transient faults. They become dangerous when the system cannot distinguish "try again" from "do the side effect again."

Check: A client library retries every 503 twice by default. Why might that be unsafe for POST /complete-lesson?

Think first, then reveal.

Answer: 503 may be transient, but the operation changes state. If the first attempt committed and the response was lost, a blind retry can duplicate side effects unless the request is idempotent or deduplicated.

Backoff Controls Failure Amplification

Now widen the incident.

The live cohort launch sends many learners through the same completion path. The progress service slows down. Gateways time out. Every gateway retries immediately.

The system creates a feedback loop:

progress service slows down
  -> gateways time out
  -> immediate retries arrive
  -> queues grow
  -> latency rises
  -> more gateways time out
  -> even more retries arrive

The recovery policy has become a load amplifier.

Backoff changes the shape of the loop by spacing attempts out. Jitter adds randomness so clients do not all retry on the same schedule. A retry budget limits how much extra work recovery is allowed to create. The caller deadline stops retries once the larger user path no longer benefits.

progress service slows down
  -> gateways time out
  -> retries wait with backoff and jitter
  -> retry budget limits extra attempts
  -> overloaded service has room to recover or shed work

Backoff does not guarantee success. It buys stability. It says, "If we try again, we will not all try again at once forever."

A small policy might look like:

attempt 1: timeout 300 ms
attempt 2: wait 50-100 ms jitter, timeout 200 ms
attempt 3: only for idempotent work, wait 150-250 ms jitter
stop: no retry if user deadline has less than 100 ms left
budget: retries may add at most 20% extra request volume per minute

The exact numbers are not universal. The review questions matter more:

Common Design Mistakes

Confusion: "Timeout means failure"

Why it is tempting:

The caller saw no answer. In local programming, no answer often feels like the function failed.

Better model:

A timeout means this caller stopped waiting. It does not prove the server did nothing. Treat side-effecting timeouts as ambiguous until idempotency, reconciliation, or evidence resolves them.

Confusion: "Retries are always reliability"

Why it is tempting:

Retries often make flaky tests or brief network hiccups disappear.

Better model:

Retries are extra work under uncertainty. They improve reliability only when operation semantics, deadlines, and dependency health make another attempt safe.

Confusion: "Backoff is just waiting longer"

Why it is tempting:

Backoff looks like a delay setting.

Better model:

Backoff is pressure control. It changes a retry storm into spaced, bounded attempts so the dependency has a chance to recover or shed load.

Practice

Review this policy for the learning platform:

Gateway rule:
  timeout every progress-service call after 250 ms
  retry every timeout twice immediately
  use no idempotency key
  ignore the remaining user deadline

The progress service has two operations:

GET /progress-summary
POST /complete-lesson

Write a short review with:

  1. one thing the policy gets right
  2. two risks
  3. one safer retry rule for the read
  4. one safer retry rule for the write

Model answer:

The policy at least prevents the gateway from waiting forever. But it treats timeout as proof of failure, retries state-changing writes without idempotency, ignores the user deadline, and can add immediate load during an overload. For GET /progress-summary, retry only transient failures while enough deadline remains, with backoff and jitter. For POST /complete-lesson, retry only if the request carries a stable idempotency key and the progress service deduplicates repeated attempts; otherwise return an uncertain result or schedule reconciliation.

Trade-offs and Limits

Timeouts improve responsiveness by bounding how long callers wait. They cost certainty because the caller may stop waiting before the server outcome is known.

Retries improve resilience to transient faults. They cost extra load and can create duplicate side effects unless the operation is safe to repeat.

Backoff, jitter, and retry budgets improve system stability. They cost some recovery speed for individual requests, especially when a quick second attempt would have worked.

This lesson does not prove whether a timeout came from packet loss, overload, crash, or partition. That is the next lesson's job. Here, the main discipline is narrower: do not turn one local symptom into unsafe recovery behavior.

You can see the boundary when the incident question changes from "Why did this request fail?" to "How much extra work did our recovery policy create while the dependency was already slow?"

Resources

Key Takeaways

PREVIOUS Serialization, Schemas, and Protocol Choices NEXT Network Partitions and Failure Models