Articles
HTTP Client Coding Interview: Deadlines, Retries, and Safe Repeated Requests
Build an HTTP client in a coding interview with total deadlines, bounded retries, Retry-After, cancellation, and safe recovery from lost responses.
In an HTTP client coding interview, a short retry loop can conceal a wrong result. The caller may wait beyond its deadline, a consumed body may be sent again, or a timed-out order may be created twice because the first response disappeared after the server committed it.
Build around two questions: how much time remains, and what makes this operation safe to repeat? Permit a repeat only when the failure policy, request replayability, operation contract and remaining budget all allow it. None of those follows simply from catching an exception.
This guide adds an original implementation exercise with a fake transport and clock. Official HTTP and library facts are cited; the worked numbers and tests are constructed practice, not candidate reports or employer requirements.
Define the contract before the loop
Ask what the client must return: a fully read response, a response stream, or a parsed domain object. That determines where the deadline and cleanup responsibility end. Returning headers while the body can hang indefinitely is a different contract from returning a completed result.
For the original exercise, the transport returns only after the entire response body is handled. Its attempt budget covers that work. The client has one total deadline, a per-attempt cap, and at most one automatic repeat. A request carries immutable method, target and payload, plus a trusted repeat-safety configuration where needed.
State the result categories before implementation: success, HTTP error, transport outcome unknown, deadline unable to accommodate further work, cancellation, and exhausted retry policy. An unknown remote write should remain unknown; calling it “failed” may encourage another unsafe submission.
These choices let you demonstrate why each send happened and why the client stopped. Do not begin with connection pooling, authentication refresh and circuit breakers unless the prompt asks for them. You can name those extensions while keeping the core contract reviewable.
Keep one deadline across every attempt
Official fact: Python's time.monotonic() is unaffected by system-clock updates and cannot move backward. Its reference point is undefined, so compare readings from the same clock rather than treating the value as a calendar timestamp. Python time documentation.
Original design: compute the deadline once, then recalculate remaining time before an attempt or wait. Use the smaller of the per-attempt cap and the remaining total budget. Never reset the total deadline after a retryable failure.
This fragment shows the budget calculation, not a complete network implementation:
deadline = clock.now_ms() + total_budget_ms
remaining = deadline - clock.now_ms()
if remaining <= 0:
return DeadlineExceeded()
attempt_budget = min(per_attempt_ms, remaining)
response = transport.send(request, attempt_budget)
The fake transport enforces its supplied budget. A real adapter must enforce the same boundary across its actual phases, including body handling under this contract. Passing a timeout option to an unfamiliar library is not proof that it covers DNS, connection setup, reads and total elapsed time alike.
Trace the budget with numbers
Original practice trace: total budget is 1,000 milliseconds, each attempt is capped at 400 milliseconds, and the chosen backoff is 200 milliseconds. The first attempt returns 503 after 350 milliseconds; the second succeeds after 300.
| Stage | Elapsed at completion | Total budget remaining | Decision |
|---|---|---|---|
| First attempt | 350 ms | 650 ms | Retryable outcome, subject to repeat safety |
| Backoff | 550 ms | 450 ms | Second attempt receives a 400 ms cap |
| Second attempt and body | 850 ms | 150 ms | Return success |
The second attempt gets 400 milliseconds, not 450, because the attempt cap still applies. The 150 milliseconds left at success is unused budget, not a reason to wait longer.
Now reduce the total budget to 700 milliseconds while preserving the same trace. After the first attempt and backoff, only 150 milliseconds remain. The fake transport cannot finish the second 300-millisecond operation within that budget, so the client terminates at 700 milliseconds with a deadline result.
This counterexample exposes a common bug: a loop that grants every attempt the original timeout. Test the transport's received budget and elapsed clock, not just whether the client eventually returned an error.
Which failures deserve a repeat?
Official fact: RFC 9110 defines idempotency by the intended effect of repeated identical requests, not identical response bytes. PUT, DELETE and safe methods are idempotent under their defined semantics. It advises against automatically repeating a non-idempotent request without knowledge that its semantics are idempotent or that the original was not applied. It also advises against automatically retrying a failed automatic retry. RFC 9110, section 9.2.2.
The exercise therefore permits one automatic repeat, with explicit operation eligibility. A retryable error alone is insufficient. A production API that violates its method semantics requires additional endpoint knowledge; the method name cannot repair that behavior.
Original policy: consider transport failures and HTTP 429, 502, 503 or 504 for a repeat. Return 400 as an HTTP error without retry. This is a teaching policy to discuss, not an HTTP rule requiring every client to retry those statuses. Apply the provider's documented semantics and the prompt's constraints before expanding it.
Official library fact: browser fetch() can resolve with an HTTP error response such as 404. Check the response status explicitly rather than relying on the promise's rejection path to classify all failures. MDN: Using Fetch.
Keep transport failure, HTTP failure and malformed response data separate. A successful status with invalid JSON may be a response-contract defect; blindly retrying it can hide the evidence. Preserve the status, attempt number and bounded diagnostic details without placing credentials in logs.
What if the write succeeded but its response was lost?
Original provider simulation: the client submits a create-order request. The provider commits one order, then loses the response. The caller sees a transport exception and cannot tell whether an order exists.
Without a repetition contract, the exercise returns an unknown outcome after that first attempt. It does not submit a second order. Explain the next recovery route: query authoritative state through a documented operation identifier or surface the unresolved result for reconciliation.
With the fake provider's explicit contract, one logical operation retains key order-1 and the same method, target and payload fingerprint. The provider stores the first result before the response disappears. A repeat using that identity returns the existing result, leaving the side-effect count at one.
Change the payload from one item to two while keeping the key, and the fake provider rejects the fingerprint conflict. Generate a fresh key on every attempt, and the duplicate-prevention argument disappears. A client must preserve operation identity, not merely populate a header.
Provider-specific official fact: Stripe documents saved results for idempotency keys, parameter comparison, and keys that may be removed after at least 24 hours. Reuse after pruning can create a new request. That is Stripe's contract, not a universal retention rule for HTTP APIs. Stripe idempotent requests.
Use that example to ask the right questions of any provider: what is the key's scope, how are conflicts handled, when does retention expire, and what happens to concurrent requests? The serial in-memory simulation answers none of those for a real service. It demonstrates only the specified fake contract.
Handle waiting, replay and cancellation deliberately
Official fact: Retry-After can contain an HTTP date or a non-negative integer delay in seconds. RFC 9110, section 10.2.3.
The original client chooses the larger of its local 200-millisecond delay and a valid server-requested delay. If that wait would consume all remaining budget, it stops without another attempt. It does not shorten a two-second server delay into a tiny wait merely to fit a one-second deadline.
For a date value, derive a delay using a wall-clock reading, then account for waiting against the monotonic deadline. In the fixture, invalid header values fall back to the local 200-millisecond delay. The fixture checks integer, date, negative and malformed cases; it does not test real server clock skew.
Official library fact: Fetch request bodies are streams and may already be consumed after sending. Prepare replayable data or a valid fresh request before a repeat; reusing a consumed request can fail. Fetch cancellation uses an AbortController signal, and aborting after headers arrive can still interrupt reading the body. MDN Fetch request and cancellation behavior.
In this exercise, immutable payload bytes are replayable. A request explicitly marked unreplayable is not repeated. Do not buffer an unbounded upload merely to enable retries; discuss a size limit or a caller-supplied body factory if the prompt introduces streaming.
Cancellation is terminal for the client. Check it before sending, during cancellable backoff and within the cooperative transport. Keep it distinct from a transient error so a catch-all retry does not restart work the caller canceled.
Inference: local cancellation cannot establish that a remote write was rolled back. A server may already have committed before the client stopped waiting. Preserve the operation identifier so cancellation and later reconciliation can describe the same work.
Make the tests observe extra work
Sixteen controls pass in the original offline Python fixture, including checks of received attempt budgets and simulated side effects. It uses a virtual clock, a scripted transport and a separate serial provider simulator. No real HTTP requests, sleep, sockets or paid provider operations are involved.
The budget controls check the 850-millisecond success trace, the shortened 700-millisecond deadline, and no attempted send when the deadline starts exhausted. Retry controls verify one-repeat maximum, HTTP 400 termination, server delay handling and cancellation before sending, during backoff and during the cooperative attempt.
Write-safety controls preserve an unknown POST result, refuse a key without a provider contract, and refuse an unreplayable body. The lost-response provider control observes two client sends but exactly one simulated side effect. A changed payload under the same key produces a conflict.
That last assertion is stronger than checking only the final 201 response: a duplicate-create implementation could return the same success status after creating two orders. Tests should observe attempted requests, supplied budgets and committed effects, because the final result alone can conceal extra work.
The fixture proves policy decisions under its stated assumptions. Before using a real adapter, test body consumption, resource cleanup, transport timeout coverage and cooperative cancellation separately. The simulation does not establish durable key storage, concurrent duplicate suppression or production timing accuracy.
Explain ownership when the interviewer adds complexity
If an SDK already retries, choose one layer to own the effective attempt budget. Two layers each allowing an initial attempt plus a repeat can permit four sends. Count lower-level attempts before claiming the application's two-attempt limit holds.
If the prompt adds parallel requests, each child should derive its available time from the parent's remaining deadline. Clarify whether a failed child cancels its siblings or returns a partial result. Do not grant every child a fresh full timeout after the parent has already spent time waiting.
Practice those extensions through these verified questions. They address adjacent engineering decisions and do not establish any employer's current coding format.
| Question | HTTP client follow-up |
|---|---|
| Build an API aggregator with concurrency and retries | Propagate a parent deadline and define partial-failure behavior |
| Design a REST API Abstraction Layer | Expose deadline, operation identity and typed outcomes clearly |
| Handle API Errors with Python Requests | Separate transport, HTTP and response-parsing errors |
| Make a Charge-Then-Record Payment Endpoint Safe Against Timeouts, Retries and Crashes | Preserve uncertainty after a committed write loses its response |
| Debug and Harden a Ticketing Backend API | Observe duplicate effects and keep backend invariants intact |
For a next practice session, use Design a REST API Abstraction Layer and demonstrate the two traces first: a shrinking deadline, then a committed write with a lost response. Explain the contract that permits each repeat and show the test that catches an extra simulated side effect, while stating what the real transport still needs to prove.