The abrupt death of a server
You deploy a new version of your API. The load balancer stops sending traffic to the old instance. The old instance is still processing a batch of heavy database queries. You send a termination signal to stop the old process. The process dies instantly. The database queries abort. The clients waiting for those responses get a connection reset error. The load balancer sees the error and marks the service as unhealthy. This is the opposite of graceful.
A graceful shutdown changes that sequence. The server stops accepting new work. It waits for the current work to finish. It cleans up its resources. It exits cleanly. The clients get their data. The load balancer sees a healthy exit. The database connections close properly. The logs flush to disk.
Go makes this pattern straightforward. The net/http package includes a Shutdown method on http.Server. It handles the hard parts. You provide the signal and the timeout. The rest is mechanics.
What graceful shutdown actually means
Think of a busy coffee shop. The manager decides to close early. A graceful shutdown means the manager stops letting people in the door. The barista finishes the lattes already being made. The cashiers finish the transactions in progress. Once the last cup is handed over and the last receipt is printed, the staff locks the door and goes home.
An abrupt shutdown is like pulling the fire alarm. Everyone stops what they are doing. Half-finished drinks go in the trash. Customers leave angry. The staff has to clean up a mess.
In Go, the http.Server.Shutdown method acts as the manager. It stops the listener immediately. New connections are rejected with a connection refused error. Existing connections are allowed to complete. The method blocks until all active connections close or a context deadline expires. If the deadline expires, the server kills the remaining connections and returns an error.
Shutdown is a contract. Honor the timeout.
The minimal pattern
Here is the skeleton. It starts a server, waits for a signal, and shuts down.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// Create a basic handler
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello"))
})
// Configure the server
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Start the server in a goroutine so it doesn't block main
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// Create a context with a timeout for the shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Perform the graceful shutdown
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exiting")
}
Convention aside: gofmt is mandatory. Don't argue about indentation. Let the tool decide. Most editors run it on save. The code above follows the standard formatting.
How the pieces fit together
The main function sets up the server and launches it in a background goroutine. ListenAndServe blocks forever under normal conditions. The main goroutine blocks on <-quit. This channel receives operating system signals. When you press Ctrl+C or a process manager sends SIGTERM, the signal lands in the channel.
The program creates a context with a five-second timeout. This context is the deadline for the shutdown. server.Shutdown stops the listener immediately. New connections are rejected. Existing connections are allowed to finish. If all connections close within five seconds, Shutdown returns nil. If a connection is still active after five seconds, Shutdown returns a context deadline exceeded error. The server then exits.
Convention aside: Context is plumbing. Run it through every long-lived call site. The context.Context always goes as the first parameter, conventionally named ctx. Functions that take a context should respect cancellation and deadlines. In this case, the shutdown context tells the server when to give up on waiting.
The signal channel pattern
The os/signal package bridges the gap between the operating system and your Go program. Operating systems use signals to communicate with processes. SIGINT is the signal sent when you press Ctrl+C. SIGTERM is the standard termination signal sent by process managers like systemd or Docker.
The signal.Notify function registers the quit channel to receive specific signals. You buffer the channel to one. This prevents a race condition where a signal arrives before the channel is ready to receive. The buffer holds the first signal. Subsequent signals are dropped. You only need one signal to trigger the shutdown.
Convention aside: Public names start with a capital letter. Private start lowercase. No keywords like public or private. The quit channel is private to main. The Server type is public because it is exported from net/http.
A realistic server with cleanup
Real servers do more than return "Hello". They hit databases, call other APIs, and process files. You need to see the wait in action. This example adds a slow handler and a cleanup step.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
// slowHandler simulates a long-running request
func slowHandler(w http.ResponseWriter, r *http.Request) {
// Simulate work that takes 3 seconds
time.Sleep(3 * time.Second)
w.Write([]byte("Done"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", slowHandler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Start server
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s", err)
}
}()
// Wait for signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down...")
// Give the server 10 seconds to finish active requests
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
// Cleanup happens here after Shutdown returns
log.Println("Cleanup complete. Exiting.")
}
The Shutdown method returns only after all active connections close. The code after Shutdown runs during the cleanup phase. This is where you close database connections, flush log files, and stop background workers. The server is no longer accepting traffic. It is safe to release resources.
Convention aside: if err != nil { return err } is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. The error check after Shutdown follows this pattern. It logs the error and exits.
Pitfalls and compiler errors
The http.ErrServerClosed error is a trap for beginners. ListenAndServe returns an error when the server stops. If you call Shutdown, ListenAndServe returns http.ErrServerClosed. This is not a failure. It is the expected result. If you treat it as a fatal error, your program crashes with a non-zero exit code even when everything worked. Check for this specific error and ignore it.
Another pitfall is the context timeout. If you set the timeout to one second but your requests take five seconds, Shutdown returns a context deadline exceeded error. The server kills the active connections. The clients see errors. Pick a timeout that matches your slowest expected request.
Goroutine leaks happen when the goroutine waits on a channel that never gets closed. Always have a cancellation path. If you spawn background workers, they must listen to the same context or signal that triggers the shutdown. Otherwise, the process hangs forever waiting for those workers to finish.
Convention aside: The receiver name is usually one or two letters matching the type. (b *Buffer) Write(...) is correct. (this *Buffer) or (self *Buffer) is not. Go does not use this or self.
The compiler complains with cannot use x (untyped int constant) as string value in argument if you pass the wrong type. It rejects the program with loop variable i captured by func literal if you forget to capture the loop variable in a closure. These errors prevent subtle bugs. Trust the compiler. Argue logic, not formatting.
Decision matrix
Use http.Server.Shutdown when you need to stop accepting new connections and wait for active requests to finish. Use http.Server.Close when you need to stop the server immediately and drop all active connections. Use a context with a timeout when you need to enforce a maximum wait time for the shutdown process. Use plain sequential code when you don't need concurrency: the simplest thing that works is usually the right thing.