Graceful shutdown in Go HTTP services
6 October 2026 · 6 min read
Every deployment, scale-down and node drain ends with your process being told to stop. If it exits immediately, any request in flight fails. Users see sporadic errors that vanish by the time you look. Go makes the fix short, but a few details are easy to miss.
The pattern
Start the server in a goroutine, wait for a termination signal, then call Server.Shutdown with a timeout. Shutdown stops accepting new connections and waits for active requests to complete.
package main
import (
"context"
"errors"
"log"
"net/http"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
srv := &http.Server{Addr: ":8080", Handler: mux}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("server error: %v", err)
}
}()
<-ctx.Done()
log.Println("shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("forced shutdown: %v", err)
}
}Details that matter
- ErrServerClosed is expected after Shutdown is called, so do not treat it as a failure
- Set a timeout shorter than your platform's kill deadline. Kubernetes defaults to 30 seconds between SIGTERM and SIGKILL
- Shutdown does not wait for hijacked connections such as WebSockets. Track and close those yourself
- Background workers need their own cancellation. Pass ctx into them and wait for them to finish with a sync.WaitGroup
Kubernetes and load balancers
There is a short window after a pod is marked terminating where traffic may still arrive, because endpoint removal propagates asynchronously. Two common mitigations: add a brief sleep or preStop hook before beginning shutdown, or have your readiness probe start failing as soon as the signal arrives so the load balancer stops sending new requests.
Set server timeouts too
A bare http.Server has no read or write timeouts, so slow or stalled clients can hold connections forever and block shutdown. At minimum set ReadHeaderTimeout, and consider ReadTimeout, WriteTimeout and IdleTimeout for your workload.
How to test it
Start the server, send a request to an endpoint that sleeps for several seconds, then send SIGTERM while it runs. The request should complete successfully and the process should then exit. Automate this as an integration test so a refactor cannot silently break it.
Want to talk it through?
Email or call us. No obligation, just a conversation about what you are trying to achieve.
Contact us →