What does an http.Server's ConnState hook report in Go, and what are its connection states?
answer
- a callback, not a method
- five states, two of them terminal
- one of them exists only because of reuse
- hijacked never reaches closed
- runs inline, so keep it cheap
basics
~20 sServer.ConnState is a callback net/http invokes on every connection state change, receiving the net.Conn and an http.ConnState. The states are StateNew, StateActive, StateIdle, StateHijacked and StateClosed. Use it to count held connections and the idle share.
solid answer
~50 s`Server.ConnState` is a `func(net.Conn, http.ConnState)` the server calls whenever a connection changes state. `StateNew` means accepted and expected to send a request; `StateActive` means at least one byte of a request has been read, and it fires before the handler runs; `StateIdle` means the request was handled and the connection is being held open for the next one; `StateHijacked` and `StateClosed` are both terminal. Its real use is instrumentation: increment atomic counters per state and you get a live gauge of open connections and the idle/active split, which is the number capacity work actually needs. Two rules: the hook runs inline on the server's own goroutines, so it must be cheap and must never block or touch the `net.Conn`; and `StateHijacked` is terminal with no `StateClosed` after it, so a naive counter that only decrements on `StateClosed` drifts upward.
code
go · 12 linesvar open atomic.Int64
srv := &http.Server{Addr: ":8080", Handler: mux}
srv.ConnState = func(_ net.Conn, s http.ConnState) {
switch s {
case http.StateNew:
open.Add(1)
case http.StateClosed, http.StateHijacked:
// StateHijacked is terminal too: no StateClosed ever follows it.
open.Add(-1)
}
}go deeper
Know that http.Server has a ConnState callback and be able to name the states, especially that a connection sits in StateIdle between requests when keep-alives are in play.
Explain when each transition fires — StateActive before the handler, StateIdle only because connections are reused — and write a counter that is correct for both terminal states.
Show you would use it to answer a real operational question: how many connections are held, and what fraction are idle. Say why the callback must be non-blocking and must not touch the connection.
Frame it as the measurement that makes a connection cap defensible rather than arbitrary, and decide what the fleet should actually alarm on before the cap starts binding.
## The hook ```go srv.ConnState = func(c net.Conn, s http.ConnState) { … } ``` `ConnState` is a field on `http.Server`, not a method. When it is non-nil the server calls it on every state transition of every connection, passing the connection and the new state. It is the only built-in view `net/http` gives you into the connection layer beneath your handlers. ## The five states * **`StateNew`** — the connection has been accepted and is expected to send a request immediately. It is reported before the connection's serving goroutine gets going. * **`StateActive`** — at least one byte of a request has been read. The documented subtlety: this fires *before* the request enters a handler, and does not fire again until that request has been handled. So `StateActive` is a per-request signal, not a per-byte one. * **`StateIdle`** — the request has been handled and the connection is being held open, waiting for the next request on the same socket. This state exists only because keep-alives exist; without reuse a connection would go from active straight to closed. * **`StateHijacked`** — a handler took the raw connection over (as a protocol upgrade does). **Terminal**: the server has washed its hands of the connection, and no `StateClosed` follows. * **`StateClosed`** — terminal, the connection is gone. The type has a `String` method, so states print readably in a log line or a metric label. ## What it is actually for Counting. A Go server that is memory-bound because it holds many connections needs to know two numbers that nothing else surfaces: how many connections are open, and how many of them are idle rather than actively serving. Both come out of this hook: ```go var open atomic.Int64 srv.ConnState = func(_ net.Conn, s http.ConnState) { switch s { case http.StateNew: open.Add(1) case http.StateClosed, http.StateHijacked: open.Add(-1) } } ``` With an ingest endpoint behind a load balancer, that gauge tells you something the request-rate graph cannot: whether 8,000 held connections are mostly idle sockets the balancer is pooling, or genuinely concurrent work. Those two situations call for completely different responses, and without the hook you are guessing. ## Two traps **Terminal-state accounting.** `StateHijacked` is terminal and is not followed by `StateClosed`. A gauge that increments on `StateNew` and decrements only on `StateClosed` will climb forever on a server that hijacks connections — an upgrade path, for instance — and you will chase a connection leak that does not exist. Decrement on both terminal states, as above. **Blocking.** The hook runs inline on the server's own goroutines: `StateNew` is delivered on the accept path before the serving goroutine is started, and the later transitions on the connection's own goroutine. Anything slow in there — a mutex under contention, a log write to a slow sink, a metrics call that allocates and blocks, a channel send to a consumer that has stalled — stalls real serving work. Keep it to atomic counter updates, or a non-blocking send to a buffered channel that a separate goroutine drains. And do not read from or write to the `net.Conn` you are handed; you are being told about a connection the server still owns, not given permission to use it. ## What it is not It is not a rate limiter and not an admission control point. Returning from it does not let you refuse a connection — `net/http` has already accepted it by the time you hear about `StateNew`, and the hook has no return value. If you want to cap connections you must do it at the listener, before `net/http` ever sees them. The hook's job is to tell you what the number is, so that whoever sets the cap is setting it from measurement rather than from a guess.
- Why does a connection gauge built on this hook drift upward on some servers?Because `StateHijacked` is terminal and is never followed by `StateClosed`. A counter that increments on `StateNew` and decrements only on `StateClosed` loses one every time a handler takes over a connection, so the gauge climbs and looks like a leak. Decrement on both terminal states.
- Can you use ConnState to refuse a connection when you already have too many?No. The hook has no return value and fires after the connection has been accepted, so there is nothing to refuse. Capping has to happen at the listener, ahead of net/http. The hook's value is telling you what the count is so the cap can be chosen from data.
- What must a ConnState callback avoid doing?Blocking, and touching the connection. It runs inline on the server's own goroutines, so a contended mutex, a slow log sink or a send on a full channel stalls accepting or serving. Atomic counters, or a non-blocking hand-off to a metrics goroutine, are the safe shapes.
saying these in an interview costs you the question
- Thinks the hook can reject or throttle new connections
- Forgets StateHijacked is terminal with no StateClosed
- Does blocking I/O or logging inside the callback
- Reads from or writes to the net.Conn it receives
- Believes StateActive fires once per byte read