Go

Implement graceful shutdown for an HTTP server with background workers.

Question 461HardGo 1.22 to 1.25

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

All 35 Standard Library, HTTP & Systems Design in Go questions