HTTP Clients, Transport & Connection Pooling
Outbound HTTP in Go flows through http.Client, which delegates connection management to http.Transport.
Search across all documentation pages
Outbound HTTP in Go flows through http.Client, which delegates connection management to http.Transport.
Tuning timeouts and pool sizes prevents goroutine leaks, TLS stalls, and connection exhaustion under load.
http.Client is the high-level API for sending requests.
Its Transport field implements RoundTripper and owns TCP dial, TLS, HTTP/2 upgrade, and idle connection reuse.
Client.Timeout bounds the entire request including body read.
Finer control lives on Transport: DialContext, TLSHandshakeTimeout, ResponseHeaderTimeout, and idle connection limits.
Every response body must be closed so connections return to the pool.
Quick-reference recipe card - copy-paste ready.
var apiClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{Timeout: 5 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 5 * time.Second,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
},
}
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := apiClient.Do(req)
if err != nil { return err }
defer resp.Body.Close()When to reach for this:
http.Get/http.Post in production code paths.package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"time"
)
type API struct {
base string
client *http.Client
}
func NewAPI(base string) *API {
return &API{
base: base,
client: &http.Client{
Timeout: 8 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 3 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 3 * time.Second,
ResponseHeaderTimeout: 4 * time.Second,
MaxIdleConns: 50,
MaxIdleConnsPerHost: 8,
IdleConnTimeout: 60 * time.Second,
},
},
}
}
func (a *API) Fetch(ctx context.Context, path string) (map[string]any, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.base+path, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
resp, err := a.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return nil, fmt.Errorf("status %d: %s", resp.StatusCode, string(b))
}
var out map[string]any
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, err
}
return out, nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
api := NewAPI("https://httpbin.org")
data, err := api.Fetch(ctx, "/get")
if err != nil {
log.Fatal(err)
}
fmt.Println(data["url"])
}What this demonstrates:
http.Client with explicit Transport tuning.NewRequestWithContext for cancellation propagation.defer resp.Body.Close() on every success and error path.Client.Do calls Transport.RoundTrip, which dials or reuses an idle connection.Body.Close().MaxIdleConnsPerHost limits reuse per upstream; excess connections close after the response.| Layer | Field | Covers |
|---|---|---|
| Whole request | Client.Timeout | Dial through body read |
| TCP dial | Dialer.Timeout | Initial connection |
| TLS | TLSHandshakeTimeout | Handshake only |
| Server think time | ResponseHeaderTimeout | After request sent, before headers |
| Context | req.Context() | Caller cancellation |
// Inject tracing or auth without replacing Transport entirely:
type roundTripper struct { base http.RoundTripper }
func (rt roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
req = req.Clone(req.Context())
req.Header.Set("Authorization", "Bearer "+token)
return rt.base.RoundTrip(req)
}
client.Transport = roundTripper{base: client.Transport}| Parameter | Type | Description |
|---|---|---|
Client.Timeout | time.Duration | Zero means no overall timeout |
MaxIdleConnsPerHost | int | Default 2 in older Go; raise for hot hosts |
IdleConnTimeout | time.Duration | Closes idle pooled connections |
DisableKeepAlives | bool | Forces new connection per request |
http.DefaultClient in servers - No timeout and shared transport across the process. Fix: Construct package-level or injected clients per dependency.resp.Body - Leaks connections until pool exhaustion. Fix: defer resp.Body.Close() immediately after successful Do.io.Copy(io.Discard, resp.Body) before close on error paths.Client.Timeout - May hide need for ResponseHeaderTimeout on slow servers. Fix: Layer transport timeouts for hung-header cases.http.DefaultTransport - Affects entire process including libraries. Fix: Clone defaults into a new Transport struct.| Alternative | Use When | Don't Use When |
|---|---|---|
resty / heimdall | Retry and backoff helpers wanted | You want zero dependencies |
| gRPC client | Strong contracts, streaming | Peer only speaks REST |
net/http + custom RoundTripper | Full control, stdlib only | You need a full HTTP client framework |
| Service mesh sidecar | mTLS and retries at data plane | Simple internal JSON calls suffice |
Client.Timeout is fixed per client instance.
Context deadline varies per call and composes with parent cancellation.
Use both: client ceiling plus per-request context budget.
Default idle limits per host are small.
High QPS to one API without raising it causes excessive TCP churn.
Yes.
Clients are safe for concurrent use once configured.
Create one per upstream dependency at process start.
Set TLSNextProto to an empty map on Transport for TLS connections, or configure ForceAttemptHTTP2 to false.
Implement retry logic in a custom RoundTripper or caller wrapper.
Respect Idempotency-Key and only retry safe methods or known idempotent paths.
Yes, up to 10 redirects by default.
Set CheckRedirect on the client to customize or disable.
Use bytes.NewReader with NewRequestWithContext, set Content-Type, and pass the reader as the body.
MaxConnsPerHost (Go 1.11+) caps concurrent connections per host.
Combine with client-side rate limiting for protection.
Set Transport.TLSClientConfig with loaded client cert and CA pool.
Rotate certs via custom GetClientCertificate callback.
Yes if timeout policies match.
Different Client.Timeout values can share one Transport.
Wrap RoundTripper to log method, URL, status, and duration.
Avoid logging bodies with secrets.
The in-flight request aborts and returns context.Canceled.
Still close any partial response body if Do returns a response.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026