go 88 lines · 2 tabs

Graceful HTTP Server Shutdown in Go with SIGINT and Context Cancellation

Shared by codesnips Jul 2026
2 tabs
package main

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

	"example.com/app/server"
)

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

	mux := http.NewServeMux()
	mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})
	mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(3 * time.Second)
		w.Write([]byte("done\n"))
	})

	srv := server.New(":8080", mux)

	go func() {
		log.Printf("listening on %s", srv.Addr())
		if err := srv.Run(); err != nil {
			log.Fatalf("server error: %v", err)
		}
	}()

	<-ctx.Done()
	stop() // restore default handler so a second Ctrl+C force-quits
	log.Println("shutdown signal received, draining connections...")

	if err := srv.Shutdown(10 * time.Second); err != nil {
		log.Fatalf("graceful shutdown failed: %v", err)
	}
	log.Println("server stopped cleanly")
}
2 files · go Explain with highlit

This snippet shows the idiomatic way to shut down a Go HTTP server without dropping in-flight requests when the process receives a SIGINT (Ctrl+C) or SIGTERM (sent by orchestrators like Kubernetes or systemd). The core problem is that calling os.Exit or letting the process die abruptly severs open connections mid-response, corrupts streaming replies, and skips cleanup of database pools and other resources. A graceful shutdown instead stops accepting new connections, lets active handlers finish within a deadline, and only then exits.

In main.go, the entry point wires two lifecycle signals into a single context.Context via signal.NotifyContext. This returns a context that is cancelled the moment either signal arrives, which is cleaner than manually managing a chan os.Signal. The stop function returned by NotifyContext must be deferred so the signal handler is unregistered on exit. The server runs in its own goroutine because srv.ListenAndServe blocks until the server stops; the main goroutine then blocks on <-ctx.Done(), waking only when a signal fires or the listener errors.

The server package holds the construction and teardown logic. New builds an *http.Server with sane production timeouts — ReadHeaderTimeout guards against slow-loris attacks and IdleTimeout reaps idle keep-alive connections. The key method is Shutdown, which derives a fresh context.WithTimeout (the incoming signal context is already cancelled, so a new deadline is needed) and calls http.Server.Shutdown. That call closes listeners, then waits for active requests to complete or the timeout to elapse, returning context.DeadlineExceeded if handlers hang.

Note the treatment of http.ErrServerClosed in Run: ListenAndServe always returns a non-nil error, and ErrServerClosed specifically means shutdown was requested, so it is filtered out rather than treated as a crash. A common pitfall is choosing too short a shutdown deadline, which cuts off legitimate long requests; another is forgetting that new SIGINTs during shutdown are ignored unless a second handler is added to force-exit. This pattern is the baseline for any long-running Go service.


Related snips

Share this code

Here's the card — post it anywhere.

Graceful HTTP Server Shutdown in Go with SIGINT and Context Cancellation — share card
Link copied