How to shut down Go apps without crashes

In Go, context.Context provides an elegant way to manage the lifecycle of long-running operations. One powerful feature it enables is the ability to cancel processes gracefully, a must-have in any microservice, CLI tool, or background job that needs to clean up resources or shut down in response to signals or external timeouts.

Why use graceful shutdown?

When building services that are expected to run for extended periods like HTTP servers, consumers or workers, it’s not enough to just run the old fashion os.Exit(1) or let the process die abruptly. You want to give your code a chance to:

  • Close DB connections
  • Flush logs or metrics
  • Stop background routines
  • Notify other systems

Go’s context.WithCancel and os/signal make this easy and idiomatic.

How to handle OS signals

There are multiple operating system signals we can interact with and all have different purposes. The ones we’re using when dealing with context cancellation are SIGINT and SIGTERM:

  1. SIGINT: is short for Signal Interrupt. It’s typically sent when a user manually interrupts a program. For example, by pressing Ctrl+C in a terminal.

    Signal information:
    • Signal number: 2
    • Source: Manual keyboard input (e.g. a developer in a terminal).
    • Default behavior: Immediately terminates the process unless handled.
  2. SIGTERM: stands for Signal Terminate. It’s the standard way the system or another process politely asks your app to shut down.

    Signal information:
    • Signal number: 15
    • Source: Operating system, Docker, Kubernetes or other external process.
    • Default behavior: Terminates the process, but gives you a chance to clean up.

Here’s how to listen for both signals and stop your process cleanly when such signals arrive.

package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // Channel to listen for OS signals
    sigs := make(chan os.Signal, 1)
    // Register the signals we want to be notified to the channel:
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

    // Trigger cancel when signal is received
    go func() {
        sig := <-sigs
        fmt.Println("Received signal:", sig)
        cancel()
    }()

    fmt.Println("Running... press Ctrl+C to stop")

    // Simulate a background job
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Context canceled. Shutting down gracefully.")
            return

        default:
            fmt.Println("Working...")
            time.Sleep(2 * time.Second)
        }
    }
}

What this does

  1. We create a context.WithCancel() and defer the cancel.
  2. We listen for OS signals (SIGINT, SIGTERM) in a goroutine.
  3. Once a signal is received, we call cancel(), which triggers ctx.Done() to unblock.
  4. Inside the loop, we listen to the context’s cancellation and exit when it’s triggered.

Considerations

Know how to handle different signals is one of the most basic skills you must have in order to develop stable and reliable applications. There are situations when you want to kill whatever resources you are using BEFORE stopping the application, to ensure there are no remaining leaking resources.

One of the most common questions you’ll face during interviews (especially for backend, DevOps, or platform roles) is exactly about this: How do you gracefully stop an application?

And it’s not just theory. This question is a subtle way of testing if you understand real-world production concerns, like:

  • What happens when a pod is terminated during a rolling update?
  • How do you prevent data loss or corruption on shutdown?
  • What’s the cleanest way to stop background workers?
  • How do you handle in-flight requests during a graceful exit?

It’s one thing to build something that “works”. It’s whole another to build something that survives a SIGTERM.

When you show that you use context.Context, that you capture SIGINT/SIGTERM and that your processes can exit without crashing, leaking memory, or leaving resources open. That’s senior-level thinking.

Whether you’re talking about Go services, Kubernetes jobs, or CLI tools: graceful shutdowns separate beginners from engineers who think in production.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top