Graceful Shutdown for Web Servers

Updated

September 8, 2026

Graceful Shutdown for Web Servers

Overview

On deploy or Ctrl-C, stop accepting new connections, finish in-flight requests, then exit. Go’s http.Server.Shutdown does the hard part—wire it to signals and a deadline.

Pattern

func main() {
    mux := http.NewServeMux()
    // register routes...

    srv := &http.Server{
        Addr:              ":8080",
        Handler:           mux,
        ReadHeaderTimeout: 5 * time.Second,
    }

    go func() {
        slog.Info("listen", "addr", srv.Addr)
        if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            slog.Error("serve", "err", err)
            os.Exit(1)
        }
    }()

    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()
    <-ctx.Done()
    slog.Info("shutdown signal")

    shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()
    if err := srv.Shutdown(shutdownCtx); err != nil {
        slog.Error("shutdown", "err", err)
        _ = srv.Close()
    }
}

Kubernetes notes

  • SIGTERM on pod stop
  • terminationGracePeriodSeconds must exceed your shutdown timeout
  • Readiness fail first (optional sleep) so LB drains before Shutdown
// optional: sleep 2s after SIGTERM so endpoints controller removes you
time.Sleep(2 * time.Second)
_ = srv.Shutdown(shutdownCtx)

In-flight work beyond HTTP

Background workers need the same root context:

root, stop := signal.NotifyContext(...)
// workers select on root.Done()
// then Shutdown HTTP

Health endpoints during drain

  • Liveness: still 200 until process dies
  • Readiness: 503 after signal so new traffic stops
var ready atomic.Bool
ready.Store(true)

mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
    if !ready.Load() {
        http.Error(w, "draining", http.StatusServiceUnavailable)
        return
    }
    w.WriteHeader(http.StatusOK)
})
// on signal: ready.Store(false)

Rules of thumb

Do Don’t
Shutdown with timeout Only os.Exit on SIGTERM
Fail readiness early Keep accepting traffic while dying
Close DB pools after HTTP drains Cut DB under active handlers

Try next

  1. Slow handler (5s sleep); send SIGTERM; confirm request completes.
  2. Set shutdown timeout 1s; prove forced close path.
  3. Add /readyz drain behavior.