Mastering Graceful Shutdown in Go: Signal Handling Best Practices

This article explains why proper signal handling is crucial for Go services, details common Unix signals, demonstrates common pitfalls, and provides a robust, context‑driven approach with code examples for graceful termination, including Kubernetes considerations.

Code Wrench
Code Wrench
Code Wrench
Mastering Graceful Shutdown in Go: Signal Handling Best Practices

Why Signal Handling Matters in Go Services

Many developers think a service only needs to run, but a production‑grade Go service must know when and how to stop gracefully. Without proper handling, requests may be cut off, logs lost, and containers reclaimed while goroutines are still running.

"Service can run, graceful exit? That's optional." – Wrong.

A mature Go service should understand system signals and perform orderly shutdown.

What Are Unix Signals?

Signals are OS notifications to a process. The most relevant for services are:

SIGINT – interrupt (Ctrl+C)

SIGTERM – termination request (kill, docker stop, K8s)

SIGKILL – forced kill (cannot be caught)

SIGHUP – configuration reload

SIGKILL cannot be caught; only SIGINT and SIGTERM can be used for graceful shutdown, so your program must handle these two signals and perform cleanup.

How Go Supports Signal Handling

The os/signal package provides the API. The simplest usage is:

sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
fmt.Println("receive signal, exiting...")

The real challenge is what to do after receiving the signal.

A Common but Dangerous Pattern

Some projects simply block on the signal channel and call os.Exit(0):

go func() {
    <-sigCh
    os.Exit(0)
}()

This skips deferred cleanup, leaves resources open, abandons goroutines, and abruptly cuts in‑flight requests.

This is not a graceful exit; it’s a premature shutdown.

Robust Solution: Drive the Whole System with Context

Use signal to cancel a context; let the context coordinate shutdown.

Complete Example

package main

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

func main() {
    // 1️⃣ Build a cancellable root context
    ctx, cancel := context.WithCancel(context.Background())

    // 2️⃣ Listen for OS signals
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        <-sigCh
        log.Println("Received exit signal, starting graceful shutdown...")
        cancel()
    }()

    // 3️⃣ Start HTTP server
    server := &http.Server{Addr: ":8080"}
    go func() {
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server start failed: %v", err)
        }
    }()

    // 4️⃣ Start background worker
    go worker(ctx)

    // 5️⃣ Wait for shutdown signal
    <-ctx.Done()

    // 6️⃣ Gracefully shut down HTTP server
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := server.Shutdown(shutdownCtx); err != nil {
        log.Printf("Server shutdown error: %v", err)
    }

    log.Println("Program exited gracefully")
}

func worker(ctx context.Context) {
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()
    for {
        select {
        case <-ctx.Done():
            log.Println("Background worker exiting")
            return
        case <-ticker.C:
            fmt.Println("Background task running...")
        }
    }
}

Key Steps Explained

Step 1: Build a cancellable root context – This acts as the global shutdown switch.

Step 2: Listen for SIGINT/SIGTERM – When a signal arrives, cancel the context.

sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
    <-sigCh
    cancel() // trigger context cancellation
}()

Step 3: Make background tasks respect the context – Workers select on ctx.Done() to stop cleanly.

func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            log.Println("Worker exiting")
            return
        default:
            // do work
        }
    }
}

Step 4: Gracefully shut down the HTTP server – Use Server.Shutdown with a timeout to allow in‑flight requests to finish.

shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(shutdownCtx)

This step distinguishes an engineering‑grade service from a simple prototype.

What to Do After Receiving SIGTERM in Real Projects

Stop accepting new requests

Wait for ongoing requests to finish

Flush logs, metrics, tracing data

Close DB / MQ / connection pools

Notify downstream consumers or leader roles

Signals are not “exit commands” but “cleanup notifications”.

A Subtle Pitfall: Don’t Block the Signal Goroutine

Doing heavy work directly after reading the signal blocks the goroutine and can delay shutdown. Instead, just trigger state change (e.g., cancel the context) and let the main flow handle cleanup.

<-sigCh
cancel()

Signal Handling in Kubernetes

Pod termination sends SIGTERM Kubernetes waits terminationGracePeriodSeconds (default 30 s)

If still running, it sends SIGKILL If you ignore SIGTERM, the pod is killed after the grace period, abruptly terminating in‑flight requests and risking data loss.

Kubernetes gives you 30 seconds; you must use them wisely.

Conclusion

Graceful signal handling reflects a system’s architectural maturity. Go provides simple primitives, but engineers must treat shutdown as a first‑class workflow, coordinating context cancellation, resource cleanup, and service termination to ensure stability.

backendKubernetesGoGraceful Shutdownsignal-handling
Code Wrench
Written by

Code Wrench

Focuses on code debugging, performance optimization, and real-world engineering, sharing efficient development tips and pitfall guides. We break down technical challenges in a down-to-earth style, helping you craft handy tools so every line of code becomes a problem‑solving weapon. 🔧💻

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.