Free learning / 50-minute HTTP retry proof workshop
Retry amplification: count the calls that actually arrive
Run two actual loopback HTTP hops. Compare nested and single-owner retries, invalid queries and transient recovery using received request records, then change the attempt limit.
No account needed. Use your own copy for writing and keep the original as a reference.
Engineering lab library / Run locally
Count both HTTP hops.
Predict six outcomes, inspect the actual received requests, then change attempts and the supplied recovery condition separately before handing over the retry decision.
Actual GETs between one caller and two owned loopback servers. Sequential bounded attempts; no SDK, external provider, business write, backoff, jitter or performance measurement.
Start here / Python 3.11+ · Standard library
Actual HTTP retry ownership and amplification
Count actual GET arrivals, compare nested with single-owner retries, stop unchanged invalid requests and change the recovery threshold.
Python 3.11+ · Standard library · Two owned loopback servers · No external service
Original LockedIn Labs proof workshop · 50 planned minutes, excluding setup · Python 3.11+ standard library. A caller contacts one local service, which contacts a second local provider fixture. Actual HTTP GETs, query validation and handler observations execute on loopback. There is no SDK, external provider, model, business write or load benchmark.
1. Separate one request from its attempts
A retry sends another attempt after a result the client considers recoverable. An attempt limit includes the initial call: three total attempts allow two retries. When a caller permits three service attempts and that service permits three provider attempts per request, one failed lookup can become nine provider requests.
The number is a bound under the stated behavior, not a universal prediction. A success or a non-retryable result can stop a loop early. Another hidden SDK, proxy or queue retry layer can change the bound. Start by identifying who owns each loop and which event consumes an attempt.
This workshop compares a deliberately broken policy with a bounded single-owner candidate. The broken policy retries every non-200 response in both layers. The candidate lets the caller alone retry a received 503, and each service request makes one provider call. This is a small read-only teaching contract, not a production retry policy to paste into a client.
Illustrative learning artifact
One event. Two transaction boundaries.
event-42 → delivery may repeat →
2. Follow the two actual HTTP hops
The runner binds two owned servers to 127.0.0.1 on temporary ports. A normal request carries GET /lookup?item=synthetic. The provider validates that query. Invalid cases omit item, so its actual query check returns 400. Always-unavailable cases return a supplied 503. Transient cases return 503 for the configured number of provider requests, then 200.
Caller-attempt and service-attempt headers correlate the local observations; they are not credentials or a distributed budget. The service forwards the unchanged query and actual status. Each provider handler records the request it received. The servers and their threads are shut down and closed after each independently reset case.
The browser mechanism is an authored explanation. Your runner report supplies the actual request evidence. No request is sent by opening this page.
UNDERSTAND THE SYSTEM
One lookup can arrive nine times downstream
Five authored frames explain actual sequential GETs between one caller and two loopback servers in one Python process. Counts come from handler observations; no browser request, SDK, jitter, deadline or load benchmark is executed here.
Watch one example, step by step.
Read the full worked example
- 01Name both loops
Three total attempts mean one initial attempt and two retries at each configured layer.
Default limits: Caller 3 · nested service 3
- 02Count the unavailable arrivals
Three outer calls each cause three provider GETs. The final 503 accompanies nine actual provider request records.
nested-unavailable: Caller→service 3 · provider 9 · final 503
- 03Inspect the unchanged invalid query
The provider actually checks the missing item and returns 400. The broken nested policy repeats it nine times without repairing the input.
nested-invalid: Same missing item · nine received 400s
- 04Set ownership and classification
The bounded candidate leaves one provider call per service entry. It sends three requests for persistent 503, but stops after the first received 400.
single-owner-unavailable / invalid: Provider 3 for 503 · provider 1 for 400
- 05Compare the same successful result
After two supplied provider failures, both transient candidates reach 200 with three provider calls. Nested ownership uses one service entry; caller ownership uses three. The final status alone does not reveal the path.
nested-transient / single-owner-transient: Same provider 3 · service entries 1 versus 3
Read the connections in plain English
- 01Calling application → Local service
The root loop can enter the service three times for one lookup.
- 02Local service → Provider fixture
An independent inner loop can send three requests per caller attempt.
- 03Provider fixture → Received request records
Each HTTP handler appends the query, correlated attempt numbers and actual returned status.
- 04Received request records → Retry ownership and classification
Nine unchanged missing-item requests still return 400; a final status hides that repeated work.
- 05Retry ownership and classification → Local service
With a single caller retry owner, the service no longer resets its own loop.
- 06Retry ownership and classification → Receiving engineer
Explain the observed work and the still-untested production contract before adapting a retry policy.
Check your understanding: Does choosing a single retry owner establish successful recovery?
No. It can bound extra work while still returning 503, or stop before a later success. Count actual arrivals, classify the received failure and identify the operation's contract. Backoff, waiting instructions, end-to-end deadlines, authorization and safe side-effect handling require separate evidence.
3. Predict and run six comparisons
Download the standalone Python runner, inspect it and run in a disposable folder:
python3 --version
python3 fde-http-retries.py > retries-original.json
python3 fde-http-retries.py --case nested-unavailable --attempts 2 > retries-nested-two.json
python3 fde-http-retries.py --case single-owner-transient --attempts 2 --failures 1 > retries-recovered.jsonUse py -3 on Windows if that is your launcher. No packages or keys are needed. The runner accepts only named cases, attempts from 1 through 4 and transient failures from 0 through 8. It accepts no user-selected host, URL or port and does not use an inherited HTTP proxy. These deliberately immediate retries stay inside the two owned local servers.
Predict both hop counts before opening the default report:
| Case | Caller → service requests | Service → provider requests | Final caller status |
|---|---|---|---|
nested-unavailable | 3 | 9 | 503 |
single-owner-unavailable | 3 | 3 | 503 |
nested-invalid | 3 | 9 | 400; unchanged missing item repeated |
single-owner-invalid | 1 | 1 | 400; no second attempt |
nested-transient | 1 | 3 | 200 after two provider failures |
single-owner-transient | 3 | 3 | 200 after two provider failures |
The final two cases have the same provider count, with different retry ownership. A successful final status does not reveal how much work occurred or where it repeated. Reducing attempts bounds extra work; it does not repair an unavailable dependency.
4. Inspect the requests rather than just the final status
Read caller_attempts, service_requests, provider_requests, counts and final_http_status. In nested-unavailable, group the provider records by caller_attempt: each outer attempt contains service attempts 1, 2 and 3. These are actual received GETs, not nine entries generated from multiplication alone.
In nested-invalid, inspect path and status. Every forwarded query still lacks item; repeating it cannot satisfy the provider's unchanged validation rule. The candidate stops on that received 400 because it classifies the status separately from choosing a retry owner. A single owner that still retries every error could continue repeating the same invalid input. This is not a malformed HTTP parser test or a claim that every 4xx status has the same recovery contract.
In transient cases, the provider's availability condition is supplied fixture behavior: after two failed valid requests, its next valid request returns found. Compare the nested service's one incoming request with the single-owner service's three. Do not infer a throughput or latency improvement from those counts; the experiment measures neither production performance nor model cost.
Check owned_servers: both should bind to loopback, have different ports and end with stopped: true. The report is emitted after cleanup. Unexpected transport or handler errors fail the run rather than being silently converted into a retryable 503. The selected cases exercise received statuses; lost responses and in-flight cancellation require separate evidence.
5. Change the limit and the recovery condition separately
With --attempts 2, always-unavailable nested retry produces four provider requests; the single-owner case produces two. With the default two transient failures, two attempts at the caller-owned layer stop at 503 before the third provider call can succeed. The nested two-by-two policy can reach the third provider request during its second outer attempt and return 200. That later success does not erase its different work budget.
Then hold attempts at two and change only --failures 1: the caller-owned transient case returns 200 after two provider requests. With failures zero it succeeds immediately. Keep original and changed reports separate, including the prediction you made before each run.
For logical deadline and shared-budget reasoning, return to the retry-budget model. Its delays and deadlines are supplied logical values. This new HTTP runner does not implement a shared cross-service budget, end-to-end deadline, backoff wait, jitter, Retry-After handling, cancellation or a fleet load test. The two exercises answer different bounded questions.
6. Give another engineer the receiving task
Allow ten minutes for receiving review. One person retains the default report. Another predicts and runs nested-unavailable with two attempts, then locates four actual provider requests. Swap roles for single-owner-invalid and the changed transient condition. Each person keeps their own command, prediction, received evidence, assistance and correction. Solo practice is self-review.
The application owner identifies its own loop and the useful request contract. The service/SDK owner inventories retries already performed by adapters or libraries. The dependency owner supplies retryable failure meaning, safe operation semantics, overload behavior and any waiting instructions. The pod lead retains the bounded work limit and the next authorized environment test. None inherits another owner's permissions or infers client readiness from this exercise.
Use a short receiving note:
Synthetic participant / exact runner and Python version:
Prediction / command / original or changed condition:
Actual caller-to-service count / provider count / final status:
Provider request records supporting that count:
My own action / assistance / first explanation and correction:
Actual retry owner / limit / error classification still to verify:
Application owner / service or SDK owner / dependency owner:
Next bounded test / receiving role / evidence still missing:7. Defend the policy in an interview
- Why can two three-attempt loops send nine provider requests? Each outer attempt starts a fresh inner loop. Both continue under the deliberately broad failure rule in that case.
- Does a single retry owner guarantee success? No. It can bound extra work while still returning 503, or stop before a later recovery. Its placement and limits need workload evidence.
- Why not retry the missing-item response? The same invalid query cannot pass the unchanged validation rule. Correct the request under its actual contract; do not generalize all error statuses from this one case.
- Does backoff remove retry amplification? Waiting can change arrival timing, but it does not erase requests already sent. Backoff, jitter, budgets and ownership need separate design and tests.
- Can this be used for an uncertain payment or tool action? This exercise performs only synthetic reads. For side effects, use the operation's authorization, idempotency and reconciliation contract before another attempt; start with the HTTP outcome workshop.
Primary references
Amazon's retry/backoff guidance discusses layered amplification and retry placement. Google SRE overload handling distinguishes request and client retry budgets. RFC 9110 defines the HTTP status semantics; the fixture supplies the availability condition. Checked 29 October 2026. The code, cases and receiving exercise are original LockedIn Labs materials, not a vendor SDK or performance claim.