Graceful shutdown
Process shutdown should have one owner that captures signals, stops new work, and gives in-flight work a bounded time to finish.
Canonical guidance
- use
signal.NotifyContextor equivalent once at process scope - stop accepting new work, then drain in-flight work with a timeout
- cancel or stop background goroutines as part of the same shutdown flow
Use when
- HTTP or gRPC servers
- queue workers
- long-lived background services
Avoid
ListenAndServewith no signal handling path- unbounded
Shutdownwaits - letting request-scoped or background work outlive the process owner accidentally
Preferred pattern
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
Anti-pattern
- catching
SIGTERMbut continuing to accept new work while “shutting down”
Explanation: This is tempting because the process is technically responding to signals, but it defeats the whole drain strategy.
Why
- graceful shutdown is lifecycle control: trigger, admission stop, drain, timeout, exit
Related pages
Sources
- os/signal package - Go Team
- net/http package - Go Team
- Go Concurrency Patterns: Context - Sameer Ajmani