Go

How does http.Client connection pooling work, and what are common mistakes with http.Client?

Question 447HardGo 1.22 to 1.25

Pooling lives in http.Transport, not Client. The Transport keeps idle keep-alive connections keyed by scheme+host+proxy. Both are safe for concurrent use and should be created once and reused.

Common mistakes:

  1. Creating a new Client/Transport per request — no reuse, a new TCP+TLS handshake each time, and leaked idle connections (each Transport has its own pool) leading to port/FD exhaustion.
  2. Using http.DefaultClient — it has Timeout: 0; a hung server blocks forever.
  3. MaxIdleConnsPerHost defaults to 2. A service making 100 concurrent calls to one backend will open and close connections constantly, piling up TIME_WAIT sockets.
  4. Not reading/closing response bodies, which prevents connection return to the pool.
var apiClient = &http.Client{
	Timeout: 10 * time.Second, // whole request incl. body read
	Transport: &http.Transport{
		Proxy:               http.ProxyFromEnvironment,
		MaxIdleConns:        200,
		MaxIdleConnsPerHost: 100,
		MaxConnsPerHost:     0, // unlimited; set to cap concurrency
		IdleConnTimeout:     90 * time.Second,
		TLSHandshakeTimeout: 5 * time.Second,
		ForceAttemptHTTP2:   true,
	},
}

Prefer per-request deadlines with http.NewRequestWithContext; Client.Timeout is a coarse upper bound. Clone the default transport with http.DefaultTransport.(*http.Transport).Clone() to keep sane dialer defaults.

More on Standard Library, HTTP & Systems Design in Go

All 35 Standard Library, HTTP & Systems Design in Go questions