skip to content

For http.Server's four timeout fields, how do you set a fleet-wide default and decide which endpoints get an exemption?

level: principalimportance: nice to knowfreq 33%

answer

  1. the default should arrive by construction
  2. one field is never negotiable
  3. raising the fleet number to fit one route
  4. exempt per response, not per fleet
  5. an exemption still carries an upper bound

basics

~20 s

Ship the default as a shared server constructor rather than a written guideline, keep the header deadline short and non-negotiable, and grant exemptions per response or per listener instead of loosening the fleet number for everyone.

solid answer

~50 s

Put the policy in code: a shared constructor that returns a configured `http.Server`, so the default arrives by using the platform's function rather than by remembering a wiki page. Split the four fields by how negotiable they are. `ReadHeaderTimeout` is short and fixed for every endpoint, because it is the only field that bounds a connection before dispatch and no legitimate client needs longer. `IdleTimeout` is set explicitly and reasoned about against whatever proxy fronts the service. `ReadTimeout` and `WriteTimeout` are the ones tied to what an endpoint actually does, so they are where exemption requests arrive. The wrong response to an exemption is raising the fleet number to cover the slowest route, which unprotects every other route. Grant it narrowly instead: relax the write deadline for that one response from inside the handler, or give the streaming workload its own listener with its own numbers, and keep an upper bound on the exempted route with `http.TimeoutHandler` where the response shape allows it.

code

go · 13 lines
go
func NewServer(addr string, h http.Handler) *http.Server {
	return &http.Server{
		Addr: addr,
		Handler: h,
		ReadHeaderTimeout: 3 * time.Second,
		ReadTimeout: 15 * time.Second,
		WriteTimeout: 15 * time.Second,
		IdleTimeout: 120 * time.Second,
	}
}

// An exempted route keeps an upper bound of its own.
mux.Handle("/report", http.TimeoutHandler(reportHandler, 60*time.Second, "report timed out"))

go deeper

for a junior

Understand that these values are a deliberate choice per service rather than a constant to copy, and that the shortest way to start a server sets none of them.

for a middle

Be able to explain which field protects which phase, so you can say what a proposed value would and would not bound if it were adopted everywhere.

for a senior

Argue a concrete set of numbers from the workload, and show how a streaming route escapes the write deadline for its own response without changing the server-wide field.

for a principal

Own the default, the exemption rule and the evidence that both still hold, including what you accept as a compensating bound and what you would refuse to sign off on at all.

## Why this is a policy rather than a setting Four durations on a struct do not look like a decision anyone owns. They become one as soon as there are many services: the numbers determine what an untrusted client can hold open, they determine which legitimate clients get cut off, and they are exactly the sort of thing that is set once by whoever wrote the service and never revisited. The output of the decision is a *default that arrives by construction* plus a *rule for exemptions*, and both need someone who can be overruled on them. ## Put the default in a constructor, not a document A guideline that says every service must set four fields will be followed by most services and quietly missed by the one written under deadline pressure, which will reach for the one-line convenience helper instead and ship a listener that bounds nothing. The durable version is a small shared function that returns a configured `http.Server` given a handler and an address. Reviewers then look for one thing — was the platform constructor used — instead of checking four numbers. The corollary is that the constructor must not be painful to escape. If the only way to serve a stream is to bypass the constructor entirely, teams will bypass it, and you will have traded a visible bad default for an invisible one. ## Split the fields by negotiability **`ReadHeaderTimeout` — short, fixed, not up for discussion.** A few seconds. It is the only field that bounds a connection before any handler is reached, so it is the one protecting the process from clients that stall before dispatch. It has nothing to do with what an endpoint computes, which is exactly why no endpoint has a case for relaxing it. Even a route granted every other exemption keeps this one. **`IdleTimeout` — explicit, reasoned against the front door.** Never leave it to inherit, especially since it falls back to `ReadTimeout` and a service that deliberately sets `ReadTimeout` to zero ends up with an unbounded idle phase. Reason about it relative to whatever proxy or load balancer keeps connections warm: giving the server the longer idle window means the proxy retires connections, and the proxy is the side that can retry cleanly rather than surfacing a failure to a user. **`ReadTimeout` and `WriteTimeout` — tied to the endpoint's own shape.** These are the numbers that come from the workload: the largest legitimate upload at the slowest legitimate rate, the largest legitimate download likewise. They are where a single fleet number is genuinely wrong for some endpoints, and therefore where the exemption process lives. ## Handling exemptions without dissolving the default The failure mode is monotonic drift. One streaming endpoint asks for a longer write budget, the fleet number goes up to cover it, and six months later the default is a number that protects nothing. Three narrower instruments avoid it: 1. **Per-response relaxation.** Since Go 1.20 a handler can adjust its own deadlines through `http.NewResponseController(w)` and its `SetWriteDeadline` method; passing the zero `time.Time` clears the deadline for that response alone. The server-wide field stays strict, and the exemption is visible in the handler that needs it — reviewable, greppable, attached to the code it applies to. 2. **A separate listener.** When a whole class of traffic is different in kind — long-lived streams, bulk uploads — a second `http.Server` on its own port with its own numbers is cleaner than one server whose values are the union of every requirement. 3. **A compensating upper bound.** An exemption should not mean unbounded. `http.TimeoutHandler` wraps a handler with a time limit and, on expiry, responds 503 Service Unavailable with the message it was given, while later writes by the wrapped handler return `http.ErrHandlerTimeout`. It buffers the response and supports neither the `Hijacker` nor the `Flusher` interface, so it cannot wrap a streaming handler — which means for the streaming case the compensating control has to be something else, such as a cap on concurrent long-lived requests and an alert on their count. ## What the decision record has to contain For each exemption: which field, what value, which endpoint, what the number was derived from, and what compensating bound applies. That last item is what makes the record reviewable — a security or platform reviewer can accept a long write budget on a download route and reject an unbounded header budget anywhere, on the same page, without relitigating the whole policy. ## Knowing whether the policy holds A policy nobody measures decays. Two cheap checks keep it honest. A slow-client probe in the load-test suite, asserting that the process goroutine count stays under a bound while it runs, catches any service that shipped without deadlines. And the goroutine count on the standing dashboard, watched against request rate rather than in isolation, catches the drift in production: the two lines should move together, and a service where goroutines climb while request rate is flat has an unbounded phase regardless of what the constructor says. ## What the tradeoff actually costs in each direction Too tight and legitimate slow clients get truncated responses with no status code to explain them — the hardest class of bug to diagnose from a support ticket, because the server's own logs show a successful handler. Too loose and the process accumulates connections that cost goroutines and descriptors until it degrades in a way no individual request explains. Stating both costs is what makes this a judgment call rather than a preference for one round number.

  • What exactly does http.TimeoutHandler do when its limit expires?
    It responds 503 Service Unavailable with the message it was constructed with, and later writes by the wrapped handler return `http.ErrHandlerTimeout`. It cancels that request's context, but it cannot stop the handler goroutine, which runs until it returns. It buffers the response and supports neither `Hijacker` nor `Flusher`.
  • Why not simply set the fleet default to the slowest endpoint's requirement?
    Because one number sized for the slowest route leaves every other route effectively unprotected, and the number only ever ratchets upward. Sizing to the common case and granting narrow exemptions keeps the protection where most of the traffic is.
  • A team wants an exemption for a server-sent-events endpoint. What do you give them?
    A relaxed write deadline for that response only, cleared from inside the handler, or its own listener if the class of traffic is large enough to justify one. Not `http.TimeoutHandler` as the compensating bound, since it buffers and does not support `Flusher`; the bound has to be a cap on concurrent streams and an alert on that count.
  • How do you know months later whether the policy is still in force?
    By measuring rather than reading. A slow-client probe in the load-test suite asserting a bound on goroutine count catches a service that shipped unbounded, and goroutine count plotted against request rate on the standing dashboard catches drift, since the two lines should move together.

saying these in an interview costs you the question

  • Raises the fleet-wide value to accommodate one slow endpoint
  • Relaxes the header deadline as part of an exemption
  • Publishes the policy as a guideline with no shared constructor
  • Grants an exemption with no compensating upper bound
  • Proposes wrapping a streaming handler in http.TimeoutHandler