skip to content

Deadlines Per Call

http.Client.Timeout bounds the whole call including reading the body, while a request context bounds one attempt, and the Transport has its own narrower timers. Interviewers ask which one fires first.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

What does Go's http.Client.Timeout field cover, and does it include reading the response body?

level: juniorimportance: must knowfreq 80%

answer

  1. one budget, not one phase
  2. the clock does not stop at Do
  3. reading the body spends the same budget
  4. zero value waits forever
  5. per call, not per program run

basics

~20 s

http.Client.Timeout bounds one whole call: DNS, dial, TLS handshake, sending the request, any redirects, and reading the response body. The clock keeps running after Do returns, so a slow body read fails too. Zero means no timeout.

solid answer

~50 s

`http.Client.Timeout` is a single wall-clock budget for one call, end to end. It starts when you call `Do` (or `Get`/`Post`) and covers name resolution, connecting, the TLS handshake, writing the request, any redirects the client follows, and — the part people miss — reading `resp.Body`. The timer is not stopped when `Do` returns: if you spend six seconds reading the body under a five-second budget, the read fails with a timeout and the body is closed under you. The zero value means no timeout at all, which is why `http.Get` and anything else on `http.DefaultClient` will happily wait forever on a server that accepts the connection and never answers. In production I build my own `&http.Client{Timeout: ...}` per dependency, and reach for a per-request context deadline when one particular call needs a tighter budget than the client's ceiling.

code

go · 11 lines
go
client := &http.Client{Timeout: 5 * time.Second}

resp, err := client.Get("https://internal.example/report")
if err != nil {
	return err
}
defer resp.Body.Close()

// Not free: a slow body read spends the same 5 seconds
// and fails with a timeout error, not a short read.
body, err := io.ReadAll(resp.Body)

go deeper

for a junior

Be ready to say what the field covers in one breath: dial, TLS, request, redirects, body read, and that its zero value means no limit. Knowing that http.Get has no timeout is the answer most screens are actually checking for.

for a middle

Explain why the clock keeps running after Do returns and what that does to an in-flight Body read, and be able to say why a streaming download is the one case where this field is the wrong tool.

for a senior

Show the operational habit: one client per dependency built at start-up, its budget derived from measured latency, and no package-level http.Get anywhere in a service. Be able to describe the goroutine and connection pile-up an unbounded client causes.

for a principal

Own the distinction between a per-call ceiling in the client and a per-invocation budget carried in a context, and be ready to say which one belongs in a library you hand to other teams versus which belongs to the caller.

## What the field is `http.Client` in Go's `net/http` package has four fields: `Transport`, `CheckRedirect`, `Jar` and `Timeout`. `Timeout` is a `time.Duration`, and its documentation describes it as "a time limit for requests made by this Client". The important word is *requests*, singular: it is a budget for one logical call, measured on the wall clock, from the moment you hand the request to the client until you are finished with the response. ## The timeline it covers A single outbound HTTP call is not one operation, it is a sequence: 1. Resolve the host name to an address. 2. Open a TCP connection (or take an idle one from the transport's connection cache). 3. If the scheme is `https`, complete the TLS handshake. 4. Write the request line, headers and body. 5. Wait for the server to send response headers. 6. Read the response body. 7. Close the body. `Timeout` spans **all of it**, plus any redirect hops the client follows on your behalf — the budget is not restarted per hop. This is what makes it the single most useful knob for an engineer who just wants their program not to hang. ## The part that surprises people Most timeout mechanisms people meet elsewhere stop once the response "arrives". Go's does not. The client arranges for the request to be aborted when the deadline passes, and that abort reaches an in-flight `Read` on `resp.Body`. Concretely: ```go client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Get(url) // returns after 100ms // ... 6 seconds of slow reading later ... b, err := io.ReadAll(resp.Body) // err: the 5s budget is already gone ``` That is a feature, not a wart: a server that sends headers immediately and then trickles the body one byte a minute is exactly the failure a naive timeout misses. But it has a direct consequence for design — **you cannot use `Client.Timeout` on a client that streams large or long-lived responses**, because the download itself is spending the budget. For those, bound the earlier phases (dial, handshake, waiting for headers) instead and police the body separately. ## The zero value is a trap `var c http.Client` has `Timeout: 0`, which means *no limit*. So does `http.DefaultClient`, which is what the package-level helpers `http.Get`, `http.Post` and `http.Head` use. `http.DefaultTransport` does bound two of the phases — its dialer has a 30-second timeout and `TLSHandshakeTimeout` is 10 seconds — but nothing bounds waiting for response headers or reading the body. A hung server therefore holds a goroutine of yours indefinitely, and a program that fans out to such a server accumulates stuck goroutines and connections until something else breaks. "Never use `http.DefaultClient` in a service" is a rule of thumb worth stating in a review. ## What it is *not* - **Not per connection.** It is per call. If a function makes three sequential calls with the same client and a 5-second `Timeout`, the worst case is roughly 15 seconds, not 5. If you need "this whole unit of work finishes in 5 seconds", you need one absolute deadline shared by every call, not a client field. - **Not per redirect hop.** Ten redirects share one budget. - **Not adjustable per call.** The field lives on the client object. Every call that client makes gets the same ceiling. To make one call tighter, build the request with `http.NewRequestWithContext` and a context carrying a deadline — both limits then apply and the earlier one wins. Nothing on the request can *raise* the client's ceiling. - **Not a guarantee about server-side work.** When your side gives up, the server may still be executing. That is a distributed-systems property, not something a client field can fix. ## Practical shape Construct one client per dependency, at start-up, with a `Timeout` chosen from that dependency's measured latency plus headroom, and share it — `http.Client` is safe for concurrent use by multiple goroutines and is designed to be reused so its transport can cache connections. Then use per-request contexts where a specific call deserves less. That gives you a documented ceiling in the code that owns the dependency, and a caller-controlled budget on top of it.

  • Does http.DefaultClient bound anything at all?
    Its `Timeout` is zero, so no overall limit. `http.DefaultTransport` still caps the dial at 30 seconds and the TLS handshake at 10, but nothing bounds waiting for response headers or reading the body. A server that accepts your connection and then goes quiet holds you forever, which is why package-level `http.Get` has no place in a long-running service.
  • If one function makes three sequential calls with the same client, is the 5-second budget shared?
    No. `Timeout` applies to each call separately, so three calls can take about fifteen seconds and still never trip it. Expressing "this whole unit of work finishes in five seconds" needs one absolute deadline in a context, passed to every request via `http.NewRequestWithContext`.
  • If Client.Timeout already covers everything, why also pass a context deadline?
    Granularity and cancellation. The client field is a fixed ceiling on every call that client makes; a request context lets one call be tighter, and lets the caller abort early on a user interrupt or a failed sibling request rather than only on expiry. Both are enforced and the earlier one wins.

saying these in an interview costs you the question

  • Says the timeout stops once the response headers arrive
  • Thinks http.DefaultClient has a sensible default timeout
  • Believes a slow body read is safe under Client.Timeout
  • Claims the budget restarts on each redirect hop
  • Treats Client.Timeout as a limit for a whole program run
open as a page

With one shared http.Client, how do you give a single outbound call a tighter deadline?

level: middleimportance: must knowfreq 66%

basics

~20 s

Build the request with http.NewRequestWithContext and a context from context.WithTimeout, then call the shared client's Do. Both budgets are enforced and the earlier one wins, so a request context can tighten Client.Timeout but never extend it.

open as a page

Your Go client sets DialContext, TLSHandshakeTimeout and ResponseHeaderTimeout, yet one fetch hangs for an hour. Why?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Those three fields bound connecting, the TLS handshake and the wait for response headers. Nothing on http.Transport bounds reading the response body, so a server that sends headers and then trickles bytes holds you indefinitely.

open as a page

A Go CLI fans out to six internal APIs in one run. Where should each call's deadline live, and who owns it?

level: principalimportance: should knowfreq 35%

basics

~20 s

Use both placements for different jobs: one http.Client per dependency whose Timeout is a ceiling owned by whoever holds that dependency's latency budget, plus one absolute deadline for the whole invocation carried in a context and passed to every request.

open as a page

Your Go HTTP client sees 2s per call while the server logs 30ms. How do you find where the time goes?

level: seniorimportance: nice to knowfreq 30%

basics

~10 s

Attach a net/http/httptrace ClientTrace to the request context and timestamp its hooks: GetConn, DNSDone, ConnectDone, TLSHandshakeDone, WroteRequest and GotFirstResponseByte. The gaps between them attribute the two seconds to a specific phase.

open as a page