A Go HTTP server should set ReadHeaderTimeout, IdleTimeout and WriteTimeout on a http.Server value, and usually ReadTimeout too. The package-level ListenAndServe helper builds a Server with none of them set, and the documented zero value means no timeout, so a slow client can hold a connection open indefinitely.
What the zero value means
The Go source documents each field on http.Server. For ReadTimeout, WriteTimeout and IdleTimeout, "a zero or negative value means there will be no timeout", and ReadHeaderTimeout falls back to ReadTimeout when it is zero. The helper function is two lines long: it creates a Server with only an address and a handler, then calls its ListenAndServe method.
That is fine on a laptop. On the internet it means a client can send one header byte every few seconds and keep a goroutine and a file descriptor busy for as long as it likes. This is the slow header pattern, often called Slowloris.
The four fields, in plain words
| Field | What it limits | Zero means |
|---|---|---|
| ReadHeaderTimeout | Time to read the request headers | Falls back to ReadTimeout, else none |
| ReadTimeout | Time to read the whole request, body included | None |
| WriteTimeout | Time before response writes time out, reset when a new request header is read | None |
| IdleTimeout | Wait for the next request on a keep alive connection | Falls back to ReadTimeout, else none |
The Go source also notes that ReadTimeout does not let a handler decide a per request deadline or upload rate, so most users prefer ReadHeaderTimeout. It is valid to use both. MaxHeaderBytes caps header size, and the source says it does not limit the request body. When it is zero, DefaultMaxHeaderBytes applies, which the source defines as 1 MB.
A starting configuration
srv := &http.Server{
Addr: ":8443",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 64 << 10,
}
These numbers are a starting point, not a standard. Choose them from your slowest legitimate request. A file upload endpoint needs a longer ReadTimeout than a JSON API, and a streaming endpoint may need WriteTimeout raised or handled per request. Measure first, then tighten.
Steps to apply this
- Replace every call to the package level ListenAndServe or ListenAndServeTLS with an explicit http.Server value.
- Set ReadHeaderTimeout first. It is the cheapest protection and rarely breaks real clients.
- Add IdleTimeout so abandoned keep alive connections are released.
- Set WriteTimeout and ReadTimeout from measured request durations, and handle long running routes separately.
- Lower MaxHeaderBytes if your clients send small headers.
- Put a body size cap on handlers that read bodies. See the post on limiting request bodies with MaxBytesReader.
If a reverse proxy sits in front, the proxy has its own limits, but the Go server should not rely on that. A direct connection from another pod, a sidecar or a misconfigured route skips the proxy.
Try it yourself
Click through it at your own pace, or open it full screen. Look at how a running web app is probed from the outside, which is where missing server limits show up.
Quick check
Does http.TimeoutHandler replace server timeouts?
No. TimeoutHandler runs a handler with a time limit and answers 503 when it is exceeded, but it does not limit how long a client takes to send headers. Use both.
Is ReadTimeout enough on its own?
It covers the whole request including the body, so it can cut off slow legitimate uploads. ReadHeaderTimeout plus a per request body policy is more flexible.
Safeguard's dependency scanning and SBOM generation cover the Go modules your service builds from, with reachability analysis to separate reachable findings, and the free scan is at gold.safeguard.sh/scan.
Sources
Last reviewed: 19 October 2026.