Go

How do you implement graceful shutdown with signal.NotifyContext, and what context mistake defeats it?

Question 286MediumGo 1.22 to 1.25

signal.NotifyContext returns a context that is canceled when one of the listed signals arrives. It is the idiomatic root for a process's lifetime.

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	srv := &http.Server{Addr: ":8080", Handler: routes()}
	go func() {
		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Fatal(err)
		}
	}()

	<-ctx.Done()
	stop() // restore default behavior: a second Ctrl+C kills immediately

	shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	if err := srv.Shutdown(shutdownCtx); err != nil {
		log.Printf("forced shutdown: %v", err)
	}
}

Key points:

  • Shutdown stops accepting new connections and waits for in-flight requests until shutdownCtx expires.
  • The shutdown context must be new, created from Background. Deriving it from the already-canceled signal context makes Shutdown return at once.
  • Mistake: setting BaseContext to return the signal context. Every in-flight request's r.Context() is then canceled the moment SIGTERM arrives, which aborts the requests you meant to drain. If handlers need a shutdown signal, pass a separate context and let them decide.
  • Background workers should receive the signal context so they stop, and main should wait for them (with a sync.WaitGroup) before exiting.

More on Context

All 35 Context questions