How does net/http's server handle concurrency internally? What are the consequences for handler code?
Question 443MediumGo 1.22 to 1.25
http.Server.Serve runs an accept loop; for every accepted net.Conn it starts one goroutine (go c.serve(ctx)). With HTTP/1.1 keep-alive, that goroutine reads requests on the connection sequentially and calls your handler for each. With HTTP/2 each stream gets its own handler goroutine, so many requests on one connection run concurrently.
Consequences:
- Handlers run concurrently: any shared state (maps, counters, caches) must be protected with a mutex,
sync/atomicor channels. - A panic in a handler is recovered by the server (it logs and closes the connection) — but a panic in a goroutine you spawn from the handler crashes the whole process.
- There is no built-in concurrency limit; under load you can get unbounded goroutines. Use a semaphore middleware, load shedding, or
netutil.LimitListener. - Goroutines you start must not use the
ResponseWriterafter the handler returns.
var hits atomic.Int64
func handler(w http.ResponseWriter, r *http.Request) {
n := hits.Add(1) // safe: handlers run concurrently
fmt.Fprintf(w, "hit %d\n", n)
}
Interviewer looks for: "goroutine per connection", awareness that handlers are concurrent, and the panic-in-spawned-goroutine gotcha.
More on Standard Library, HTTP & Systems Design in Go
- Q444Explain the Go 1.22 ServeMux enhancements: method matching, wildcards, and precedence rules.
- Q445Write a logging middleware that records the response status code. What are the pitfalls of wrapping http.ResponseWriter?
- Q446Why is http.ListenAndServe(":8080", nil) not production-ready? Explain each server timeout.
- Q447How does http.Client connection pooling work, and what are common mistakes with http.Client?
- Q448Why must you close resp.Body, and why is closing alone sometimes not enough?
- Q449How is context used in net/http on both server and client sides? What happens when a client disconnects mid-request?