Skip to content
ML Academy

Core day 52 of 60 · Week 11 · Part II · application depth

LLMOps

The assistant calls a tool that adds a note to a booking. Attempt one gets a 429, attempt two times out, attempt three succeeds, and the booking shows two notes. Which failures were safe to retry, which were not, and what must the trace contain to prove it?

By the end: For one simulated LLM request you classify each failure, say which attempts were safe to repeat and why, name the fields a trace needs to reconstruct what happened, and state the gate and rollback path for a prompt release.

At a glance

Classify each failure by the state it leaves, retry in one layer, protect every write with a key or a read-back, and release prompts like code.

  • Refused or unknown

    A 429 did no work; a timeout may have. Only the first is always safe to retry.

  • One retry owner

    Retries at several layers multiply: 3 retries at each of 3 layers allow up to 64 calls.

  • Key names the intent

    Reuse one idempotency key across attempts of one action; a new key per attempt protects nothing.

  • Trace every attempt

    Model, tokens, finish reason, attempt, release id and key on every span.

  • Release by slice

    Version prompt, model, settings and index together; canary, gate on slices, roll back by alias.

Recall · 5 min

Without notes: what makes a tool call safe to repeat (Day 49), and what must an agent do first after a timeout on a call that writes something?

Open due reviews

Sign in to record this block.

Concept · 10 min

An LLM feature is a distributed system with a model in the middle. Every step can fail, and the useful question is not "did it fail?" but "what state is the world in, and is trying again safe?" A 429 was refused before any work. A timeout means you do not know: the write may have run and only the reply was lost.

A 429 is safe to retry, a timeout after a write is not, until you know what state the world is in

Predict first

A write call times out after the service already stored the record. A blind retry without a key leaves how many records?

Text description of the figure

A simulated request timeline with injectable failures (429, timeouts, 500, 400) and a selectable retry policy. Each attempt shows its status, backoff wait and whether the side effect ran, so you can see the same script produce one record or two depending on the policy.

One request, its stages, and where each one fails Diagram
Gateway +contextquota, cache,promptModel callstreams tokensValidateschema, finishreasonTool writechanges the world429 / 5xx / 529nothing ran: back off,one layerError mid-streammark partial, count it,never cacheTimeoutmay have written: samekey or read backrefusedstream breaksno reply
  1. Gateway + contextquota, cache, prompt
  2. Model callstreams tokens
    • to Validate
  3. 429 / 5xx / 529nothing ran: back off, one layer
  4. Validateschema, finish reason
    • to Tool write
  5. Error mid-streammark partial, count it, never cache
  6. Tool writechanges the world
  7. Timeoutmay have written: same key or read back

Reads and generation only cost tokens when repeated. The last stage changes the outside world, so it needs the most care.

Classify the signal by the state it leaves, then act Hierarchy
refusedinvalidrate limitedbudget spentprovider errorstate unknownstream brokeError signalwhat state is the worldin?Nothing ranbut retrying is not alwaysuseful400 invalid requestyour bug: fix it, neverretry unchanged429 + retry-afterwait at least retry-after,retry429, no retry-afterspend cap: alert anddegrade5xx or 529often transient: back off,retry or fail overTimeout, lost replysame key, or read backfirstError after 200mark partial, count as afailure
  • Error signalwhat state is the world in?
    • refusedNothing ranbut retrying is not always useful
      • invalid400 invalid requestyour bug: fix it, never retry unchanged
      • rate limited429 + retry-afterwait at least retry-after, retry
      • budget spent429, no retry-afterspend cap: alert and degrade
    • provider error5xx or 529often transient: back off, retry or fail over
    • state unknownTimeout, lost replysame key, or read back first
    • stream brokeError after 200mark partial, count as a failure

Four outcome classes: refused, failed, unknown, partial. Codes are from one provider's documentation (as read 2026-10-11); other vendors use other codes, but the classes carry over.

Is this attempt safe to repeat? Diagram
Refused beforework?Back off, retryhonour retry-after, capCould it havewritten?Retry freelya read: costs tokensonlySame key everyattempt?Retry, same keyservice replays theresultRead state backthen decide; no blindresendyesnonoyesyesno
  1. Refused before work?
    • yes: to Back off, retry
  2. Could it have written?
    • no: to Retry freely
  3. Same key every attempt?
    • yes: to Retry, same key
  4. Read state backthen decide; no blind resend
  5. Back off, retryhonour retry-after, cap
  6. Retry freelya read: costs tokens only
  7. Retry, same keyservice replays the result

A 400 never enters this chart: it is your bug, and repeating it changes nothing. A timeout is an unknown, not a failure: record it as its own outcome and resolve it with the same key or a read-back.

Retries at several layers multiply, and only when it hurts Illustration
Full outage: every attempt failsUser request1 requestJob queue3 attemptsGateway9 attemptsSDK → provider27 provider calls

Each outer attempt restarts every inner layer, so attempts multiply. With 2 retries per layer, three layers make 27 provider calls for one request.

Calls per request vs provider failure rate12481632640%25%50%75%100%Share of provider calls that fail (log y)416645% failing: 1.05 calls90% failing: 10.0 calls (3 layers)1 retrying layer2 retrying layers3 retrying layers

With 3 retries per layer, amplification is invisible on a normal day and explodes as the provider nears a full outage, exactly when it can least absorb extra load.

Computed: exact expected attempts for nested layers, each retrying a fixed number of times, with provider failures independent across calls (a simplification: real outages are correlated, which makes the right-hand end more likely).

Attempts multiply across layers, they do not add. The worst case (1 + r) per layer, multiplied, is reached in a full outage, when every layer retries into the same limit.

  • Key terms: backoff with jitter, Retry amplification, Idempotency key, Trace and span, Regression gate.
  • Classify, then act: refused, failed, unknown or partial. The error tree below is the map.
  • Retries live in one layer, with jittered backoff, a cap, a deadline and respect for retry-after.
  • Writes need a key reused on every attempt of one intended action, or a read-back before resending.
  • One release = prompt, model id, decoding settings, retrieval parameters and index, gated on slices and rolled back by alias.

Assumptions

  • The downstream tool is idempotent by design or accepts a key; if neither holds, a read-back is the only safe retry.

  • Error semantics (codes, retry-after, limits, cache rules) are as your provider documents them today; they differ by vendor and change.

  • Traces store metadata by default; prompts and answers need consent, redaction and retention rules (Day 53).

  • The golden set covers the slices a release could break.

Sign in to record this block.

Application and workplace decision · 10 min

  • Replay the opening incident against a simulated provider: attempt 1 returns 429, attempt 2 times out after the write ran, attempt 3 returns 200.
  • Three policies on the same script: blind (retry everything), keyed (one idempotency key on every attempt), read-back (after a lost reply, read the state instead of resending).
  • No API is called; outcomes are scripted and waits are random draws.
  • Predict the notes each policy leaves, then run. The last line prints worst-case provider calls when three layers each allow three retries.

Python

import random

random.seed(7)                                   # simulated outcomes: no API call is made

class NoteService:                               # fake downstream service that stores booking notes
    def __init__(self):
        self.notes, self.seen = 0, set()

    def write(self, key):
        if key is not None and key in self.seen: # same idempotency key: replay, do not write again
            return
        self.notes += 1
        if key is not None:
            self.seen.add(key)

def run(policy, script=("429", "lost_reply", "200")):
    svc, waited = NoteService(), 0.0
    key = "req-7f3a/add-note" if policy == "keyed" else None
    for attempt, status in enumerate(script, start=1):
        if status != "429":                      # 429 is rejected before any work; the others ran the write
            svc.write(key)
        if status == "200" or (status == "lost_reply" and policy == "read-back"):
            break                                # read-back: state is read, not resent
        wait = random.uniform(0, min(8.0, 0.5 * 2 ** attempt))   # exponential backoff, full jitter
        waited += wait
        print(f"    attempt {attempt}: {status:10s} backoff {wait:.2f}s")
    return svc.notes, waited

for policy in ("blind", "keyed", "read-back"):
    print(f"policy {policy}:")
    notes, waited = run(policy)
    print(f"  -> notes written {notes}, time spent backing off {waited:.2f}s")
print("worst-case provider calls per user request:", (1 + 3) ** 3)

This code is executed in the repository's checks (pnpm lesson:py / lesson:sql). It is not run on this site; copy it into your notebook.

Sign in to record this block.

Mini-assessment · 8 min

Your first answer is your evidence. You can re-answer later in practice mode; that is labelled practice.

Question 1 · easy

A streamed answer starts arriving with HTTP status 200, then an error event arrives before the end-of-message event. How should the system record this request?
Options

How confident are you? (optional, used for calibration)

Question 2 · medium

An agent's `issue_refund` tool call times out after 30 seconds. The refund API supports idempotency keys, and the agent sent key `ref-1042` on the first call. What should the retry do?
Options

How confident are you? (optional, used for calibration)

Question 3 · hard

An incident trace summary (simulated) shows 1,000 user requests produced 4,700 provider calls in ten minutes, and 3,000 of those calls returned 429 with a retry-after value. Only the SDK honoured retry-after. The gateway and the job queue each retried immediately, up to three times. Quality metrics are unchanged and nothing was released. Which change addresses the cause?
Options

How confident are you? (optional, used for calibration)

Question 4 · medium

A user request passes through three layers that each retry on the same provider error with no shared budget. The SDK retries up to 3 times, the gateway up to 2 times and the job queue up to 4 times. What is the worst-case number of provider calls for one user request, counting every layer's first attempt?
provider calls

How confident are you? (optional, used for calibration)

Question 5 · hard

Synthetic incident: on Tuesday the refund slice of the golden set falls from 56 to 49 of 60 and the faithfulness judge agrees. Your release log shows no deploy since prompt v14 passed its canary three weeks ago. Retrieval parameters and the index are unchanged, and the golden questions are frozen. The model is called through a name that the provider can repoint to a newer snapshot. What is the best next step?
Options

How confident are you? (optional, used for calibration)

Sign in to complete lessons and keep your progress.

Technical depth · optional · 25 min in total

Open only what you need. Nothing here is required to complete the day; the assessment covers the core above.

The retry process as algebra: expected attempts under a failure rate5 minThe geometric-series derivation of expected attempts and success probability under a retry cap, worked by hand at three failure rates and verified by simulation.
Attempt i happens only if every earlier attempt failed Hierarchy
succeeds, 1−ffails, fsucceeds, 1−ffails, fsucceeds, 1−ffails, fAttempt 1always madeDone after 1 callAttempt 2reached with probability fDone after 2 callsAttempt 3 = cap mreached with probabilityf²Done after 3 callsGive upprobability f³ = f^m
  • Attempt 1always made
    • succeeds, 1−fDone after 1 call
    • fails, fAttempt 2reached with probability f
      • succeeds, 1−fDone after 2 calls
      • fails, fAttempt 3 = cap mreached with probability f²
        • succeeds, 1−fDone after 3 calls
        • fails, fGive upprobability f³ = f^m

Drawn for m = 3 under independent, constant f. Summing the reach probabilities 1 + f + f² gives E[A]; the give-up leaf gives P(success) = 1 − f^m. The same tree fits any retried dependency: an OCR service for claims documents, a payment gateway or an LLM provider.

Expected attempts under a failure rate, derived Formula
E[A]=∑i=1mPr⁡(attempt i happens)=∑i=1mf i−1=1−fm1−f\begin{aligned} \mathbb{E}[A] &= \sum_{i=1}^{m} \Pr(\text{attempt } i \text{ happens}) \\ &= \sum_{i=1}^{m} f^{\,i-1} = \frac{1 - f^{m}}{1 - f} \end{aligned}
ff
probability one attempt fails with a retryable error (assumed constant and independent)
mm
maximum attempts = 1 + retry cap; attempt i happens only if the first i - 1 all failed, hence f^(i-1)
∑fi−1\sum f^{i-1}
a finite geometric series; the closed form follows from (1 - f) x (1 + f + ... + f^(m-1)) = 1 - f^m
1/(1−f)1/(1-f)
the uncapped limit as m grows: at f = 0.5 a retrying client averages 2 calls, at f = 0.9 it averages 10

The same series gives the success probability 1 - f^m. As f approaches 1 (a full outage) E[A] approaches m: every request spends its whole retry budget, which is exactly when the provider can least afford it. With L layers each capped at m attempts, the outage worst case multiplies to m^L.

Worked by hand: m = 4 attempts (3 retries) at three failure rates Table
Failure rate fE[A] = (1 - f^4)/(1 - f)P(success) = 1 - f^4Calls per success = E[A]/P = 1/(1 - f)
0.2 (normal)(1 - 0.0016)/0.8 = 1.250.9981.25
0.9 (incident)(1 - 0.656)/0.1 = 3.440.34410
1.0 (outage)4 (the cap; take the limit)0infinite: only the cap and the deadline stop it

Simulation (numpy default_rng(0), 200,000 requests): mean attempts 1.249 and 3.437, success 0.998 and 0.345. In the last column the f^m terms cancel: calls per success are 1/(1 - f) whatever the cap. At f = 0.9 every success costs ten provider calls, which is why a retry budget stops retrying long before the math does.

What a retry cap buys: much at low f, little in an incident Chart
00.250.50.75112343 retries: 34%f = 0.2 (normal) — Expected calls per request: 1, P(success): 0.8f = 0.2 (normal) — Expected calls per request: 1.2, P(success): 0.96f = 0.2 (normal) — Expected calls per request: 1.24, P(success): 0.992f = 0.2 (normal) — Expected calls per request: 1.248, P(success): 0.998f = 0.2 (normal) — Expected calls per request: 1.25, P(success): 1f = 0.2 (normal)f = 0.9 (incident) — Expected calls per request: 1, P(success): 0.1f = 0.9 (incident) — Expected calls per request: 1.9, P(success): 0.19f = 0.9 (incident) — Expected calls per request: 2.71, P(success): 0.271f = 0.9 (incident) — Expected calls per request: 3.439, P(success): 0.344f = 0.9 (incident) — Expected calls per request: 4.095, P(success): 0.41f = 0.9 (incident) — Expected calls per request: 4.686, P(success): 0.469f = 0.9 (incident)Expected calls per requestP(success)
  • f = 0.2 (normal)
  • f = 0.9 (incident)

Each point adds one retry (cap r = 0 to 5), from E[A] = (1 − f^m)/(1 − f) and 1 − f^m. At f = 0.2 one retry already gives 0.96 and three give 0.998; at f = 0.9 three retries buy 0.34 for 3.4x the calls. Past a measured f, failing fast and degrading beats retrying.

The fragile assumption: in an incident, f rises with retry volume Loop
independencebreaks: retriesfeed f1Provider saturatedshared limit, everyclient hits it2f risesfailures correlate acrossclients3Clients retryevery layer, often insync4More callsper request, toward m perlayer5Load > capacitysame users, many morecallsrejectstriggersadds callsadds loadsaturates it
  1. Provider saturatedshared limit, every client hits itrejects ↓
  2. f risesfailures correlate across clientstriggers ↓
  3. Clients retryevery layer, often in syncadds calls ↓
  4. More callsper request, toward m per layeradds load ↓
  5. Load > capacitysame users, many more callssaturates it ↓
  6. ↺ back to 1 · Provider saturated

This feedback loop is the retry storm. The tree and table above assume independent attempts; the fan-out demo shows the multi-layer version, and jitter, retry budgets and deadlines are what break the loop.

Backoff, jitter and deadlines3 minThe wait formula, why it is random, and what stops the loop.
Exponential backoff with full jitter Formula
wn∼Uniform(0, min⁡(c, b⋅2n))w_n \sim \mathrm{Uniform}\left(0,\ \min(c,\ b \cdot 2^{n})\right)
wnw_n
wait before retry n
bb
base delay (0.5 s in the application code)
cc
cap on the wait (8 s in the code), so waits stop growing
Uniform\mathrm{Uniform}
random draw: clients that failed together do not retry together

If the provider sends retry-after, wait at least that long: it is the provider saying when capacity returns. A per-request deadline stops the loop whatever the attempt count.

Upper bound of the wait per attempt (b = 0.5 s, cap = 8 s) Chart
02468123456min(8, 0.5 x 2^n) — Attempt n: 1, Maximum wait in seconds: 1min(8, 0.5 x 2^n) — Attempt n: 2, Maximum wait in seconds: 2min(8, 0.5 x 2^n) — Attempt n: 3, Maximum wait in seconds: 4min(8, 0.5 x 2^n) — Attempt n: 4, Maximum wait in seconds: 8min(8, 0.5 x 2^n) — Attempt n: 5, Maximum wait in seconds: 8min(8, 0.5 x 2^n) — Attempt n: 6, Maximum wait in seconds: 8Attempt nMaximum wait in seconds

The actual wait is a uniform draw between zero and this bound.

Why the wait is random: 1,000 clients retrying after one outage Illustration
No jitter025050075010000s4s8s12s16sTime since the outage beganpeak 1000 / 100 ms · 4/160 windows busy

No jitter: every client computes the same wait, so 1,000 retries land in the same instant, four times over.

Equal jitter025050075010000s4s8s12s16sTime since the outage beganpeak 208 / 100 ms · 119/160 windows busy

Equal jitter: wait = half the bound plus a random half. Spikes become plateaus.

Full jitter025050075010000s4s8s12s16sTime since the outage beganpeak 153 / 100 ms · 142/160 windows busy

Full jitter: wait drawn uniformly from zero to the bound. The same 4,000 retries arrive as a low, spread load.

Simulated with the trace widget's retry code: 1,000 clients, 5 attempts, base 1 s, multiplier 2, cap 8 s, no retry-after, seeded draws. The total number of retries is identical in all three panels; only their timing differs.

Identical waits synchronise clients into spikes that can knock a recovering service over again. Jitter keeps the retry count and spreads the timing.

Retry budgets, deadlines, breakers and fallbacksadvanced3 minThe controls that keep a provider incident from becoming your own outage, and how each one is misused.
Four controls that stop retries from becoming the outage Comparison
CriterionRetry budgetDeadline propagationCircuit breakerFallback or degrade
What it limitsRetries as a share of requests, per processWork done after the caller has given upCalls to a dependency that keeps failingUser impact while the provider is unavailable
TriggerBudget spent: fail fast instead of retryingRemaining time passed down with the requestFailure rate over a window; probe before closingTyped error from the retry owner
MisusePer request only: a fleet can still multiply loadFixed inner timeouts longer than the outer deadlineOpens on 400s, which are your bugs, not overloadA fallback model never put through the release gate

Retry budgets and deadline propagation follow the SRE chapter on cascading failures; circuit breakers and fallbacks are common practitioner patterns. Measure amplification directly: provider calls per user request, per layer, on every dashboard.

Idempotency keys in practice3 minWhat the server does with a key, which actions need one, and what to do when no key exists.
What the service does with an idempotency key Diagram
Request + keykey names theintentKey seen inwindow?Return stored resultno second writeDo the workstore key + resultRespondsame answer either wayyesnofirst resultstored result
  1. Request + keykey names the intent
  2. Key seen in window?
    • yes: to Return stored result
  3. Do the workstore key + result
    • first result: to Respond
  4. Return stored resultno second write
  5. Respondsame answer either way

One documented implementation checks the last 24 hours (as read 2026-10-11). Many services also reject a reused key with a different body.

Which actions need a key Overview
  • Naturally idempotent

    Set status to X, read an order or a claim, generate text. Repeating gives the same world.

  • Needs a key

    Append a note, send an email, issue a refund, charge a card, create a ticket.

  • Key scope

    Unique per intended action across users; identical on every retry of that action.

  • No key available

    Read the state back after an unknown outcome before any resend.

Same failure script, three policies (application output) Table
PolicyAttempt 1: 429Attempt 2: lost replyNotes written
Blind retryNo work doneWrite ran, resent anyway2 (duplicate)
KeyedNo work doneResent with the same key: replayed1
Read-backNo work doneState read, nothing resent1
What a trace must contain3 minSpans per stage, the fields an on-call engineer needs, and the privacy default.
One trace per request, one span per stage Stack
  1. Tracerequest id, user scope, release id, total latency, outcome (ok, failed, unknown, partial)
  2. Gateway spanquota decision, deadline, retry owner, attempt count
  3. Retrieval spanindex version, k, passage ids (not text by default)
  4. Model spanmodel id, input and output tokens, finish reason, time to first token, attempt
  5. Tool spantool name, idempotency key, status, write confirmed or unknown

Store metadata by default. Prompt and answer text need consent, redaction and a retention rule (Day 53). Attribute names can follow the OpenTelemetry GenAI conventions, which are still changing (vendor facts below).

Releases, canaries and rollbacks4 minVersion the whole configuration, gate on slices, canary, and handle a quality drop with no release.
A prompt release, gated like code Diagram
Candidatereleaseprompt, model,indexAll slicesabove floor?Canarysmall share, release idon tracesCanary holds?Promotealias movesHoldbaseline stays liveRoll backone alias changeyesnoyesno
  1. Candidate releaseprompt, model, index
  2. All slices above floor?
    • yes: to Canary
  3. Holdbaseline stays live
  4. Canarysmall share, release id on traces
  5. Canary holds?
    • yes: to Promote
  6. Roll backone alias change
  7. Promotealias moves

The candidate first passes every slice floor on the golden set; the canary is then compared with the baseline on slices, retries and cost per request, with the release id on every trace.

Hands-on lab: the candidate wins overall and still fails a slice floor Chart
  • Overall baseline0.883 pass rate
  • Overall candidate0.942 pass rate
  • Refund baseline0.85 pass rate
  • Refund candidatefloor 0.88, agreed before the run: hold0.85 pass rate
  • General candidate0.975 pass rate
  • Baggage candidate0.9 pass rate

Synthetic golden-set draws (seed 11). A gate on the overall rate alone would have shipped it.

A slice floor in a small canary is close to a coin flip Illustration
0%25%50%75%100%1050100150200250300350400Requests from this slice seen by the canary73% caught6% false rollback94% caught1% false rollback100% caught0% false rollback
  • regressed release (true pass rate 0.84) is rolled back
  • good release (true pass rate 0.95) is rolled back anyway

Exact binomial probabilities for hypothetical pass rates. A slice that gets 5% of a small canary's traffic may need days to reach a few hundred requests: size the canary per slice, not per total.

Rule: roll back if the canary's slice pass rate falls below 0.9. A release that truly passes 0.84 should be caught; one that passes 0.95 should not.

The probability that the floor rule rolls back a regressed release, and a good one, as the canary sees more requests from that slice.

Quality moved and nothing was released Sequence
  1. Freeze the inputs

    Re-run the frozen golden questions; inputs cannot explain a change on identical inputs.

  2. Diff the traces

    Model id, finish reasons, token counts and latency before and after the change.

  3. Check what can move under you

    Undated model aliases, provider defaults, index refresh jobs, upstream documents.

  4. Pin and re-gate

    Pin a dated model version, make it part of the release, re-run the slice gate.

Caching without serving the wrong answer2 minExact-match, semantic and provider prompt caches reuse different things and fail differently.
Three things called a cache Comparison
CriterionExact-match response cacheSemantic cacheProvider prompt cache
What it reusesA stored answer for an identical normalised request, user scope and release idAn answer to a similar questionProcessing of an identical prompt prefix
SavesThe whole model callThe whole model callInput price and time to first token; output unchanged
Main riskStale answers after a release if the key omits the release idServes an answer written for someone else's parametersAny change early in the prefix invalidates everything after it

Restrict semantic caching to static, non-personal content. Never cache a partial stream or an answer that failed validation. Prompt caching is not a response cache (Day 54).

Vendor facts, datedadvanced2 minThe documented provider behaviour this lesson leans on, with the date it was read.
Vendor facts behind this lesson, as read 2026-10-11 Table
FactSource in the registry
Typed errors 400, 429, 500, 504, 529; a request id on every responseClaude API errors docs
Official SDK retries transient errors twice by default, with backoff, honouring retry-afterClaude API errors docs
Errors can arrive after a streamed 200Claude API errors docs
Spend-cap 429 carries no retry-after and keeps failingClaude rate limits docs
At least 60 days' notice before a public model is retired; retired models failClaude model deprecations docs
Same idempotency key honoured within 24 hours (one email API)Resend idempotency docs
GenAI span attributes (request model, token usage, finish reasons) have status Development; content attributes are opt-inOpenTelemetry GenAI semantic conventions

Re-check before relying on them. One vendor is a documented example, not a recommendation; other providers differ.

Memory anchor

Know the state before you retry; one retry owner; the key names the intent.

Useful for

  • Deciding which errors to retry, back off on, fail over from or surface to a user, in a SaaS support assistant as much as in any service that writes.

  • Writing trace requirements that let an on-call engineer explain a duplicate action or a slow request.

  • Designing a release gate and a rollback path for prompt, model and retrieval changes.

Not suitable for

  • Judging answer quality; traces show what happened, a golden set shows whether it was good.

  • Offline batch jobs, which need job-level timeouts and restart logic instead of per-request deadlines.

Traps

A new idempotency key per attempt

Every retry looks like a new action, so the key adds a field to the request and prevents nothing.

How to avoid it

Generate the key once per intended action (for example run id plus step number) and reuse it on every attempt.

Treating the status code as the outcome

A 200 on a stream or a timeout on a write is recorded as success or failure although the real outcome is partial or unknown; an insurance team that records a timed-out claim payout as "failed" discovers the double payment from the customer.

How to avoid it

Record the final stream event and the confirmed side-effect state, and keep unknown as a distinct outcome.

Senior reasoning

A senior reads an incident by asking what state the world is in after each attempt. They keep exactly one layer in charge of retries, make every side effect safe to repeat or checkable afterwards, put the release id and the key in every trace, and refuse to ship a prompt change that passes on average but breaks a slice.

Interview question

Design the retry and rollback behaviour of a retail bank's customer-facing assistant that can reverse card fees.

Recall cards scheduled after completion

  • Why is a 429 safe to retry but a timeout on a write is not, and what makes the second attempt safe?
  • What goes in one LLM trace, and why must retries sit in one layer?

Reviewed on a 1 · 3 · 7 · 14 · 30-day schedule; a miss returns the card to 1 day.

References

Optional sections

Deep dive · 10 min

Idempotency in practice. A key identifies one intended action. The server stores the key with the result for a limited window (one documented implementation checks the last 24 hours) and returns the stored result for a repeated key. Keys must be unique per action across users and reused across retries; many designs also compare the request body and reject a key reused with a different one. Natural idempotency (set status to X) needs no key; "append a note" and "send an email" do.

Backoff. Full jitter draws the wait uniformly between zero and an exponentially growing cap, which spreads retries from many clients instead of synchronising them. Honour retry-after when present, and add a retry budget so that a share of failing requests cannot become a multiple of normal load.

Streaming. The status line is sent before the body. Decide what the user interface shows when an error arrives after text has started, and never write partial output to a cache or to a downstream system.

Caching. Prefer exact-match caching keyed on the normalised request, user scope and release id. A semantic cache that serves similar questions can return an answer written for someone else's parameters; restrict it to static, non-personal content. Provider prompt caching reuses an identical prefix and is not a response cache, so stable content goes first.

Releases and models. Treat prompt, model id, decoding settings, retrieval parameters and index as one versioned release; stamp its id on traces. Pin dated model versions and watch deprecation schedules. When a model is replaced, run the golden set on the candidate well before the retirement date.

Error-budget thinking. Pick a few signals (success rate, p95 latency, cost per request, refusal rate, retry rate) per release and alert on user-visible symptoms. Detail on latency decomposition is Day 54.

Hands-on lab: A slice-level regression gate for a prompt release · 15 min

The code draws synthetic golden-set outcomes for a baseline release and a candidate release across three slices, applies a gate (candidate must not fall below the baseline overall and each slice must stay at or above a floor), and prints the decision. Run it, then (1) explain why a gate on the overall rate alone would have shipped the candidate, (2) change the refund floor to 0.85, note what flips, and say why a floor chosen after seeing the result is a problem, and (3) add a rule that blocks a release when the candidate's canary retry rate exceeds the baseline's by more than 20%.

import numpy as np

rng = np.random.default_rng(11)
slices = {"general": 80, "refund": 20, "baggage": 20}          # synthetic golden set, 120 cases
base_p = {"general": 0.88, "refund": 0.90, "baggage": 0.85}    # pass probability, baseline release
cand_p = {"general": 0.95, "refund": 0.75, "baggage": 0.90}    # candidate: better on average, worse on refunds
floor = {"general": 0.80, "refund": 0.88, "baggage": 0.80}     # per-slice gates agreed before the run

def passes(p):
    return {s: int((rng.random(n) < p[s]).sum()) for s, n in slices.items()}

base, cand = passes(base_p), passes(cand_p)
total = sum(slices.values())
print("overall baseline", sum(base.values()) / total, "candidate", sum(cand.values()) / total)
ok = sum(cand.values()) >= sum(base.values())
for s, n in slices.items():
    rate = cand[s] / n
    flag = "ok" if rate >= floor[s] else "BELOW FLOOR"
    ok = ok and rate >= floor[s]
    print(f"{s:8s} baseline {base[s]}/{n} candidate {cand[s]}/{n} floor {floor[s]:.2f} {flag}")
print("gate decision:", "ship to canary" if ok else "hold; keep baseline release live")
Interview practice · 10 min

An e-commerce assistant's gateway shows a spike in duplicate orders after a provider incident. What do you look for and what do you change?

Content version 6 · last verified 2026-10-13