Go Web Frameworks: net/http, Chi, Gin, Echo and Fiber · Building Production APIs in Go
Production Ready: Config, Logging, Timeouts and Graceful Shutdown
Turn a working API into a service you can operate: configuration from the environment, structured logs with log/slog, server timeouts, health checks, and graceful shutdown that lets in-flight requests finish.
What you'll build
The main function of a production Go API: reads config from environment variables, logs JSON with log/slog, sets server timeouts, exposes /healthz, and on SIGTERM stops accepting new requests while finishing the ones in progress.
The idea
- Configuration from environment variables (twelve-factor style), with defaults and validation at startup: fail fast if something required is missing.
- Structured logging with the standard
log/slogpackage: JSON lines with fields (method,path,status,request_id) that log tools can search. - Timeouts on
http.Server. The zero values mean no timeout, which lets slow or malicious clients hold connections forever. SetReadHeaderTimeout,ReadTimeout,WriteTimeoutandIdleTimeout. - Health checks:
/healthz(the process is alive) and often/readyz(dependencies like the database are reachable) for load balancers and Kubernetes. - Graceful shutdown: on SIGINT/SIGTERM, call
server.Shutdown(ctx). It stops accepting new connections, waits for active requests to finish (up to a deadline), then returns. Deployments then never cut requests off mid-way.
The code
package main
import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"os"
"time"
)
type Config struct {
Addr string
ShutdownTimeout time.Duration
}
func loadConfig(getenv func(string) string) (Config, error) {
cfg := Config{Addr: "127.0.0.1:0", ShutdownTimeout: 10 * time.Second}
if v := getenv("SHUTDOWN_TIMEOUT"); v != "" {
d, err := time.ParseDuration(v)
if err != nil {
return cfg, fmt.Errorf("SHUTDOWN_TIMEOUT: %w", err)
}
cfg.ShutdownTimeout = d
}
return cfg, nil
}
func main() {
// JSON logs; the time field is dropped here only so the output below is identical on every run.
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
}))
if _, err := loadConfig(func(string) string { return "ten seconds" }); err != nil {
logger.Error("bad config", "err", err)
}
cfg, _ := loadConfig(func(k string) string { return map[string]string{"SHUTDOWN_TIMEOUT": "2s"}[k] })
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "ok") })
mux.HandleFunc("GET /report", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(300 * time.Millisecond) // a slow request that is in flight during shutdown
io.WriteString(w, "report ready")
})
srv := &http.Server{
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
ln, _ := net.Listen("tcp", cfg.Addr)
go srv.Serve(ln)
base := "http://" + ln.Addr().String()
logger.Info("listening", "shutdown_timeout", cfg.ShutdownTimeout.String())
res, _ := http.Get(base + "/healthz")
body, _ := io.ReadAll(res.Body)
logger.Info("health check", "status", res.StatusCode, "body", string(body))
done := make(chan string)
go func() { // a client request that is still running when shutdown starts
res, err := http.Get(base + "/report")
if err != nil {
done <- "error: " + err.Error()
return
}
b, _ := io.ReadAll(res.Body)
done <- fmt.Sprintf("%d %s", res.StatusCode, b)
}()
time.Sleep(100 * time.Millisecond)
// In a real service this follows signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM).
logger.Info("shutting down")
ctx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
logger.Error("forced shutdown", "err", err)
}
logger.Info("in-flight request finished", "result", <-done)
_, err := http.Get(base + "/healthz")
var opErr *net.OpError
logger.Info("new request after shutdown", "refused", errors.As(err, &opErr))
}
Output
{"level":"ERROR","msg":"bad config","err":"SHUTDOWN_TIMEOUT: time: invalid duration \"ten seconds\""}
{"level":"INFO","msg":"listening","shutdown_timeout":"2s"}
{"level":"INFO","msg":"health check","status":200,"body":"ok"}
{"level":"INFO","msg":"shutting down"}
{"level":"INFO","msg":"in-flight request finished","result":"200 report ready"}
{"level":"INFO","msg":"new request after shutdown","refused":true}Run this on your computer — see “Get Started” for setup.
The slow /report request started before shutdown and still completed with 200; new connections after Shutdown are refused. That's what lets you deploy without dropping requests.
Common mistakes
http.ListenAndServe(addr, mux)in production. It uses a server with no timeouts; build anhttp.Serverwith timeouts.- Exiting on SIGTERM immediately, cutting off in-flight requests during every deploy.
- Unstructured logs (
log.Printf("user %s did %s")) that are hard to search; useslogfields. - Reading config all over the code with
os.Getenv; load and validate it once at startup. - Health checks that hit every dependency and fail the whole fleet when one downstream is slow; keep liveness simple.
Key points
- Config from the environment, validated at startup.
log/slogJSON logs with fields; include request IDs.- Always set
http.Servertimeouts. server.Shutdown(ctx)on SIGTERM drains in-flight requests; add/healthzand/readyz.
Further reading: log/slog, http.Server.Shutdown, The twelve-factor app: config.
Practice questions
What does http.Server.Shutdown do?
Answer
It stops the server from accepting new connections, closes idle connections, and waits for active requests to finish until the context deadline expires, then returns. Call it on SIGTERM so deploys don't cut requests off.
Why is http.ListenAndServe with default settings risky in production?
Answer
The default server has no read, write or idle timeouts, so slow or malicious clients can hold connections and goroutines open indefinitely (e.g. slowloris). Configure an http.Server with ReadHeaderTimeout, ReadTimeout, WriteTimeout and IdleTimeout.
Open in the app to mark lessons complete, take notes and track your progress.