net/http Best Practices
Framework-free HTTP services that scale and shut down cleanly.
Apply these rules in code review, service templates, and load tests so every stdlib HTTP surface behaves predictably under production traffic.
How to Use This List
- Tick rules as your service template adopts them.
- Encode Tier A items in CI review bots or arch unit tests where possible.
- Pair timeout rules with documented SLO tables per upstream dependency.
- Revisit when adding gateways, WebSockets, or new outbound APIs.
A - Server and routing
- Construct
http.NewServeMux()inmain, nothttp.DefaultServeMux. Prevents import-side route collisions across packages. - Set
ReadHeaderTimeouton every productionhttp.Server. Blocks slowloris-style header stalls before handlers run. - Configure
IdleTimeoutto reclaim keep-alive connections. Stops descriptor leaks on quiet clients. - Use Go 1.22+ method and path patterns for REST routes. Keeps method matching declarative and testable.
- Limit POST bodies with
http.MaxBytesReader. Rejects oversized payloads before JSON decode work.
B - Graceful lifecycle
- Handle SIGINT/SIGTERM and call
server.Shutdown(ctx). Rolling deploys drain in-flight work instead of force-killing. - Bound shutdown context to orchestrator grace period. Avoid hanging past Kubernetes
terminationGracePeriodSeconds. - Stop accepting new work before closing dependencies. Shut down HTTP listener first, then database pools and clients.
- Expose cheap
/healthwithout auth middleware. Load balancers need a fast liveness signal. - Document streaming routes exempt from
WriteTimeout. SSE and long polls need explicit timeout exceptions.
C - Handlers and context
- Pass
r.Context()as the first argument to all blocking downstream calls. Honors client disconnect and server deadlines. - Return quietly on
context.Canceledwhen the response cannot flush. Avoids misleading 500s after the client left. - Set
Content-Typebefore writing JSON bodies. Prevents ambiguous responses and broken clients. - Call
WriteHeaderonce; usehttp.Errorfor failures. FirstWritelocks status to 200 if headers were not sent. - Close or fully read
r.Bodyon every path. Required for keep-alive connection reuse on the server side.
D - Middleware
- Place panic recovery outermost in the middleware chain. Inner panics become 500 responses without process exit.
- Authenticate before business handlers but after recovery/logging. Failed auth short-circuits without touching domain logic.
- Wrap
ResponseWriterto capture status for metrics and access logs. Default writer hides final status from middleware. - Store request metadata in context with unexported key types. Avoids string-key collisions across libraries.
- Allowlist health and metrics paths in auth middleware. Probes should not need service credentials.
E - Outbound clients
- Inject one
http.Clientper upstream dependency. Isolates timeouts and transport tuning per SLA. - Never call
http.Getfrom handlers.http.DefaultClienthas no timeout and is process-global. - Always
defer resp.Body.Close()afterclient.Do. Leaked bodies exhaust connection pools. - Drain error response bodies before close when reusing keep-alive.
io.Copy(io.Discard, resp.Body)on non-2xx paths. - Set
MaxIdleConnsPerHostfor hot upstreams. Reduces TLS and TCP churn under steady QPS.
F - Proxies, TLS, and HTTP/2
- Set custom
Transportonhttputil.ReverseProxy. Default transport is shared and untuned for gateway paths. - Implement
ErrorHandleron reverse proxies. Maps dial and header timeouts to 502/504 consistently. - Set
tls.Config.MinVersionto TLS 1.2 or higher. Documents acceptable cipher posture for external listeners. - Know whether pods speak HTTP/2, h2c, or HTTP/1.1 behind ingress. Misconfiguration shows up as latent multiplexing bugs.
- Trust
X-Forwarded-*only from known load balancers. Prevents client-spoofed IP and scheme metadata.
FAQs
Which rules should block merge in review?
Dedicated mux, ReadHeaderTimeout, graceful shutdown, context propagation, and client body closing are CI-worthy blockers.
Document exceptions in the service README.
Do these apply when using gin or chi?
Yes.
Frameworks wrap net/http; timeouts, shutdown, and client hygiene remain application responsibilities.
How do best practices relate to the production checklist?
The checklist is deploy-gate verification.
This list is ongoing team policy for templates and review.
What is the top mistake for new Go HTTP services?
Running ListenAndServe with nil handler on DefaultServeMux and no timeouts.
Fix with explicit http.Server and owned mux.
Should we share one http.Client for all upstreams?
Only if SLA and timeout needs match.
Different upstreams should get different clients and transports.
How do we enforce body size limits?
Wrap with http.MaxBytesReader per handler and return 413 on overflow.
Log limit breaches at warn level.
Where do request IDs fit?
Generate or accept X-Request-ID in outer middleware.
Propagate to logs and outbound RoundTripper wrappers.
Are these rules enough for public internet exposure?
They cover stdlib HTTP fundamentals.
Add WAF, rate limiting, and authz at ingress or mesh layers as threat models require.
Related
- net/http: Go's Batteries-Included HTTP Stack - conceptual anchor
- net/http Basics - runnable starting examples
- Production net/http Configuration Checklist - pre-deploy verification
- HTTP/2, Timeouts & ReverseProxy - timeout and proxy depth
- Middleware Chains & Request Context - middleware ordering
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).