go 35 lines · 1 tab

Graceful shutdown: draining HTTP + background workers

Leah Thompson Jan 2026
1 tab
package main

import (
  "context"
  "log"
  "net/http"
  "os"
  "os/signal"
  "sync"
  "syscall"
  "time"
)

func main() {
  srv := &http.Server{Addr: ":8080", Handler: http.DefaultServeMux}

  var wg sync.WaitGroup
  wg.Add(1)
  go func() {
    defer wg.Done()
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
      log.Fatalf("listen: %v", err)
    }
  }()

  sigCh := make(chan os.Signal, 1)
  signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
  <-sigCh

  ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  defer cancel()

  _ = srv.Shutdown(ctx)
  wg.Wait()
}
1 file · go Explain with highlit

A clean shutdown is part of reliability, not an afterthought. The pattern I like is: (1) start servers and workers, (2) listen for SIGINT/SIGTERM, (3) call Shutdown with a deadline, and (4) wait for background goroutines to finish. http.Server.Shutdown stops accepting new connections and drains in-flight requests, but only if your handlers respect context cancellation. I also prefer a single context.WithCancel that gets triggered by the signal, and then each subsystem gets a derived timeout. This avoids hung deployments where pods never terminate, and it prevents partial work from being abandoned without a chance to finish or roll back.


Related snips

Share this code

Here's the card — post it anywhere.

Graceful shutdown: draining HTTP + background workers — share card
Link copied