Implement graceful shutdown for an HTTP server with background workers.
Goal: on SIGINT/SIGTERM (Kubernetes sends SIGTERM, then SIGKILL after the grace period), stop accepting connections, let in-flight requests finish, stop workers, and flush resources — all within a deadline.
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
srv := &http.Server{
Addr: ":8080",
Handler: newMux(),
ReadHeaderTimeout: 5 * time.Second,
// Do NOT set BaseContext to the signal ctx: every in-flight
// request would be canceled the instant SIGTERM arrives.
}
var wg sync.WaitGroup
wg.Add(1)
go func() { defer wg.Done(); runWorker(ctx) }() // exits on ctx.Done()
errCh := make(chan error, 1)
go func() {
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
errCh <- err
}
close(errCh)
}()
select {
case err := <-errCh:
log.Printf("server error: %v", err)
case <-ctx.Done():
log.Println("shutdown signal received")
}
stop() // a second Ctrl+C now kills immediately
shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("forced shutdown: %v", err)
srv.Close()
}
wg.Wait()
}
Key points: ListenAndServe returns ErrServerClosed immediately when Shutdown is called — don't exit main then, wait for Shutdown to return. Shutdown does not wait for hijacked connections (WebSockets); use RegisterOnShutdown. The shutdown context must be fresh (the signal context is already canceled) — and for the same reason, don't derive request contexts from the signal context via BaseContext, or Shutdown's "let in-flight requests finish" is defeated. In Kubernetes, fail the readiness probe first and sleep briefly so the load balancer drains.
More on Standard Library, HTTP & Systems Design in Go
- Q459Compose io primitives: upload a file to HTTP while computing its SHA-256 and limiting size, without buffering it in memory.
- Q460What are the pitfalls of bufio.Scanner and bufio.Writer?
- Q462How does log/slog work? Discuss handlers, attributes, LogValuer, and performance.
- Q463What does this print? Explain the monotonic clock and time.Time comparison.
- Q464time.Ticker, time.After and timers: what leaks, and what changed in Go 1.23?
- Q465What does this print? (time.Duration arithmetic gotchas)