How do you implement graceful shutdown for a Go HTTP server?
Quick Answer
Graceful shutdown means stopping the server from accepting new connections while letting in-flight requests finish, instead of killing them abruptly when the process receives a termination signal. The pattern: listen for SIGINT/SIGTERM using os/signal, and when one arrives, call server.Shutdown(ctx) (from a *http.Server, not the package-level http.ListenAndServe), which stops accepting new connections immediately and blocks until active requests complete or a context deadline you provide is reached, whichever comes first. This is essential for zero-downtime deploys and rolling restarts in production — without it, a deploy or scale-down event can abruptly cut off requests that were mid-flight, returning errors to real users.
Detailed Answer
Without graceful shutdown, every deploy or pod restart risks abruptly cutting off requests that were already in progress.
The full pattern
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", healthHandler)
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// Wait for an interrupt or termination signal
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
log.Println("shutting down gracefully...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("forced shutdown: %v", err)
}
log.Println("server stopped")
}
What Shutdown actually does
- Immediately stops the listener from accepting new connections.
- Waits for all currently active requests to finish naturally.
- Returns once every request has completed, or once the provided
ctxdeadline is reached, whichever happens first.
If the 10-second deadline passes with requests still in flight, Shutdown returns a context-deadline error, and those specific requests get cut off. The timeout exists to bound how long shutdown can take, not to guarantee every request finishes no matter what.
Why srv.ListenAndServe, not the package-level shortcut
http.ListenAndServe(":8080", mux) // no way to call Shutdown on this later
The package-level http.ListenAndServe function has no handle you can call Shutdown on afterward. Production code should always construct an explicit *http.Server, exactly so it's possible to shut it down gracefully later.
Why this matters operationally
Kubernetes (and most orchestrators) send SIGTERM before killing a pod, with a grace period before a forceful SIGKILL. A server that doesn't handle SIGTERM gracefully will drop in-flight requests on every single rolling deploy or pod eviction. That's a real, user-visible reliability problem in production, not just a theoretical concern.