When you hit Ctrl+C on a Go service, it often drops in-flight requests, leaving users with 502s or incomplete data.
This happens because the default signal handler for SIGINT (which Ctrl+C sends) immediately terminates the process. Any goroutines handling active HTTP requests, network connections, or background tasks are abruptly killed, regardless of whether they’re halfway through a critical operation.
Here’s how to make your Go services shut down cleanly, ensuring no requests are dropped:
1. Detect the Shutdown Signal
First, you need to catch the operating system’s shutdown signals. Go’s os/signal package is perfect for this. You’ll create a channel that receives these signals.
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
// ... your service setup ...
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop // Block until a signal is received
fmt.Println("Shutting down server...")
This code creates a channel stop and registers it to receive os.Interrupt (generated by Ctrl+C) and syscall.SIGTERM (sent by kill commands or container orchestrators like Kubernetes). The <-stop line will pause execution until one of these signals arrives.
2. Stop Accepting New Connections
Once you’ve received a shutdown signal, the very next thing you must do is tell your HTTP server to stop accepting new incoming connections. This prevents any new requests from arriving while you’re in the middle of shutting down.
// Assuming you have an http.Server instance named 'srv'
if err := srv.Shutdown(context.Background()); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
The srv.Shutdown() method is the key here. It closes the listener, meaning no new connections can be established. Importantly, it doesn’t immediately close existing connections. Instead, it returns an error if the shutdown takes longer than the context’s deadline (which we’ve set to context.Background() for now, meaning it won’t timeout on its own).
3. Wait for In-Flight Requests to Complete
This is where the magic happens. srv.Shutdown() also waits for all currently active requests to finish. However, you typically want to impose a timeout on this waiting period. You don’t want your shutdown to hang indefinitely if a single request is taking an eternity.
// Modified Shutdown with a timeout
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel() // Release resources associated with the context
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Server shutdown timed out or failed: %v", err)
}
By using context.WithTimeout, you give the server a maximum of 15 seconds to gracefully finish all ongoing requests. If any request is still being processed after 15 seconds, srv.Shutdown will return an error (specifically, context.DeadlineExceeded), and your log.Fatalf will catch it. This ensures your service doesn’t get stuck.
4. Handle Other Background Tasks
Your service might have other background goroutines or tasks that aren’t directly tied to HTTP requests. These could be database cleanup jobs, message queue consumers, or periodic workers. You need to signal these to stop as well.
The common pattern is to use a context.Context that gets cancelled when the shutdown signal is received. You pass this context down to your background tasks, and they periodically check if the context has been cancelled.
// In your main function:
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // Ensure cancel is called
// Start your background worker, passing the context
go runBackgroundWorker(ctx)
// ... when shutdown signal is received ...
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
fmt.Println("Shutting down server...")
// Cancel the context to signal background tasks
cancel()
// ... then shut down HTTP server ...
if err := srv.Shutdown(ctx); err != nil { /* ... */ }
fmt.Println("Server gracefully stopped")
// In your background worker function:
func runBackgroundWorker(ctx context.Context) {
for {
select {
case <-ctx.Done(): // Check if the context has been cancelled
fmt.Println("Background worker stopping...")
return // Exit the goroutine
case <-time.After(5*time.Second): // Simulate work
fmt.Println("Doing background work...")
// Perform your actual background task here
}
}
}
The ctx.Done() channel will be closed when cancel() is called. Your background worker can then detect this and exit its loop cleanly. The time.After(5*time.Second) is just an example to show that the worker is periodically doing something; your actual work would go there.
Putting It All Together
Here’s a more complete, albeit simplified, example:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// Setup HTTP Server
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Simulate a long-running request
time.Sleep(10 * time.Second)
fmt.Fprintln(w, "Hello, graceful shutdown!")
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Goroutine to start the server
go func() {
log.Println("Server starting on :8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Could not listen on :8080: %v\n", err)
}
}()
// Setup graceful shutdown
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
// Block until a signal is received
<-stop
log.Println("Shutdown signal received. Initiating graceful shutdown...")
// Create a context with a timeout for the shutdown
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel() // Ensure cancel is called to release resources
// Shutdown the HTTP server
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Server shutdown failed: %v", err)
}
log.Println("Server gracefully stopped")
}
When you run this and hit Ctrl+C while the / handler is still sleeping (simulating a long request), the server will wait for that 10-second sleep to finish before exiting, rather than cutting it off. If a request took longer than 15 seconds, srv.Shutdown(ctx) would return an error, and the log.Fatalf would trigger.
The next hurdle you’ll face is managing application-level shutdown logic that might need to coordinate with the HTTP server’s shutdown, such as flushing buffered logs or closing database connections.