Setting a timeout does not give you a bounded operation. It gives you a number. Whether that number ever turns into an actual limit depends on three things the documentation rarely mentions: which phase of the call the timeout applies to, how much budget is left by the time the call runs, and whether the process really stops work when the limit fires. Most timeout bugs live in the gap between the number and the behavior, and they fail silently — the operation completes, just later than every promise you made upstream.

This article is a field guide to closing that gap. It covers the three timeout types that are actually different from each other, why per-call timeouts compose into unbounded totals, how to carry a deadline through a call graph instead, and what has to happen after a deadline fires for the whole mechanism to mean anything. None of it requires new infrastructure. All of it is a change in where the number is chosen and what the code does with it.

The Three Timeouts That Are Actually Different

When an HTTP client says "timeout", it usually means one of two things. A connect timeout bounds the phase before the request is sent: DNS resolution, TCP handshake, TLS negotiation. A read timeout bounds the silence between bytes once the request is in flight. Neither bounds the whole operation. A call can use its entire connect budget, then sit under its read budget, and the user experiences a duration that is the sum of both — or worse, the product, if the code retries.