Graceful Shutdown
Cleanly shut down servers and background workers by listening for signals and using context cancellation.
Contributors
When to Use
- In HTTP servers that need to finish processing active requests before stopping
- When background workers must complete in-flight tasks before exiting
- In production services that require zero downtime deployments
- When you need to flush metrics, logs, or close database connections cleanly
When NOT to Use
- In simple CLI tools that exit immediately
- For stateless workers that can be killed without consequences
- When immediate termination is acceptable
Graceful Shutdown ensures your Go application stops cleanly by listening for termination signals (SIGINT, SIGTERM) and giving running operations time to complete. This prevents data loss, request failures, and inconsistent state during deployments or shutdowns.
Implementation
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
type Server struct {
httpServer *http.Server
workers []Worker
}
type Worker interface {
Start(ctx context.Context) error
Shutdown(ctx context.Context) error
}
// BackgroundWorker processes jobs in the background
type BackgroundWorker struct {
name string
}
func (w *BackgroundWorker) Start(ctx context.Context) error {
log.Printf("Starting worker: %s", w.name)
for {
select {
case <-ctx.Done():
log.Printf("Worker %s received shutdown signal", w.name)
return nil
case <-time.After(2 * time.Second):
log.Printf("Worker %s processing...", w.name)
}
}
}
func (w *BackgroundWorker) Shutdown(ctx context.Context) error {
log.Printf("Worker %s shutting down gracefully", w.name)
// Perform cleanup: flush buffers, save state, etc.
time.Sleep(500 * time.Millisecond) // Simulate cleanup
return nil
}
func NewServer(addr string, workers ...Worker) *Server {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Simulate work
time.Sleep(3 * time.Second)
fmt.Fprintf(w, "Request completed at %s\n", time.Now().Format(time.RFC3339))
})
return &Server{
httpServer: &http.Server{
Addr: addr,
Handler: mux,
},
workers: workers,
}
}
func (s *Server) Run() error {
// Create context that will be cancelled on shutdown signal
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Channel to listen for interrupt signals
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// Start background workers
for _, worker := range s.workers {
w := worker // Capture loop variable
go func() {
if err := w.Start(ctx); err != nil {
log.Printf("Worker error: %v", err)
}
}()
}
// Start HTTP server in a goroutine
serverErrors := make(chan error, 1)
go func() {
log.Printf("Server listening on %s", s.httpServer.Addr)
serverErrors <- s.httpServer.ListenAndServe()
}()
// Block until we receive a signal or server error
select {
case err := <-serverErrors:
return fmt.Errorf("server error: %w", err)
case sig := <-quit:
log.Printf("Received signal: %v. Starting graceful shutdown...", sig)
// Cancel context to stop workers
cancel()
// Give workers time to finish
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
// Shutdown HTTP server
log.Println("Shutting down HTTP server...")
if err := s.httpServer.Shutdown(shutdownCtx); err != nil {
log.Printf("HTTP server shutdown error: %v", err)
return err
}
// Shutdown workers
for _, worker := range s.workers {
if err := worker.Shutdown(shutdownCtx); err != nil {
log.Printf("Worker shutdown error: %v", err)
}
}
log.Println("Graceful shutdown complete")
return nil
}
}
Usage
package main
import (
"log"
)
func main() {
// Create workers
worker1 := &BackgroundWorker{name: "metrics-flusher"}
worker2 := &BackgroundWorker{name: "event-processor"}
// Create server with workers
server := NewServer(":8080", worker1, worker2)
// Run server (blocks until shutdown)
if err := server.Run(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed: %v", err)
}
}
// Test it:
// 1. Run the server: go run main.go
// 2. Make a long request: curl http://localhost:8080
// 3. Press Ctrl+C while request is running
// 4. Server waits for request to complete before shutting down
Benefits
- Zero request failures: Active requests complete before shutdown
- Data integrity: Workers can flush buffers and save state properly
- Clean resource cleanup: Database connections, file handles close correctly
- Production ready: Enables zero-downtime deployments with load balancers
- Predictable behavior: Timeout enforcement prevents hanging forever
Common Gotchas
- Always set reasonable timeout durations (5-30 seconds typical)
- Remember
http.Server.Shutdown()waits for active connections, not idle keep-alives - Use
ListenAndServein a goroutine so signal handling isn’t blocked - Cancel context for workers before shutting down HTTP to stop new work