Skip to Content
Course content

226: Graceful Shutdown Patterns

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're running a busy restaurant. When closing time hits at 10 PM, you don't just walk over to the breaker box and flip the power switch, plunging everyone into darkness while they're halfway through their entrees. That would be a disaster. Instead, you follow a process: you stop seating new guests, you let the people currently eating finish their meals, you settle the bills, and then—only when the room is empty—do you lock the doors and turn off the lights.

In Go, a "hard kill" (like a SIGKILL or a sudden crash) is that power switch. A "graceful shutdown" is the professional closing process. If you just let your program terminate the moment it receives a stop signal, you might leave a database transaction half-finished, drop a file write mid-stream, or cut off a user who was just about to hit "submit" on a payment.

Catching the Signal from the OS

Your program doesn't just know it's time to die; the Operating System tells it via signals. Usually, this is SIGINT (when you hit Ctrl+C) or SIGTERM (when Kubernetes or systemd tells your app to wrap it up). To handle this, we use a channel to listen for these specific signals.

package main

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

func main() {
	// Create a channel to listen for OS signals
	stop := make(chan os.Signal, 1)
	// Notify this channel when we receive SIGINT or SIGTERM
	signal.Notify(stop, os.Interrupt, syscall.SIGTERM)

	srv := &http.Server{
		Addr: ":8080",
	}

	// Run the server in a goroutine so it doesn't block the signal listener
	go func() {
		fmt.Println("Server starting on :8080...")
		if err := srv.ListenAndServe(); err != http.ErrServerClosed {
			fmt.Printf("ListenAndServe error: %v\n", err)
		}
	}()

	// This line blocks until a signal is received
	<-stop
	fmt.Println("\nShutdown signal received. Cleaning up...")
}

Giving the Server a Deadline

Now we're at the "stop seating new guests" phase. The http.Server type in Go has a built-in Shutdown() method. It tells the server to stop accepting new requests and waits for existing ones to finish. But here's the catch: what if a request is hung or taking forever? You can't wait indefinitely, or your deployment pipeline will time out and kill you anyway.

I always use a context.WithTimeout here. It's basically saying, "I'll give you 30 seconds to finish your work; after that, I'm pulling the plug regardless."

	// Create a deadline for the shutdown process
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Shutdown gracefully shuts down the server without interrupting active connections.
	if err := srv.Shutdown(ctx); err != nil {
		fmt.Printf("Server forced to shutdown: %v\n", err)
	}

	fmt.Println("Server exited cleanly")

Mapping the Logic Back to the Restaurant

To make sure this clicks, let's map those code blocks directly back to our analogy:

  • signal.Notify(...): This is the manager checking the clock and realizing it's 10 PM.
  • srv.Shutdown(ctx): This is the act of locking the front door. No one else can come in, but the people inside aren't being kicked out yet.
  • The Context Timeout: This is the manager saying, "I'm giving the last table 30 minutes to finish their dessert, or I'm turning the lights off anyway."
  • os.Exit(0) (Implicit at end of main): Finally locking the door and going home.

One quick tip: if you're using a database connection pool (like *sql.DB), make sure you call db.Close() after srv.Shutdown(). You want the server to finish processing the requests using the database before you kill the database connection. Order of operations matters here.




📋 Practical Task

Exercise: Implementing a Graceful Shutdown for a Payment Processing Server

You are maintaining a payment gateway service. The server has a simulated "heavy" endpoint /process-payment that takes 5 seconds to complete. Currently, if you hit Ctrl+C while a payment is processing, the server dies immediately, which would cause "ghost transactions" in a real system.

Your Goal: Modify the provided boilerplate code to implement a graceful shutdown pattern. Your implementation must:

  1. Listen for SIGINT and SIGTERM signals.
  2. Use srv.Shutdown() with a context timeout of 10 seconds.
  3. Ensure that if a payment request is currently running when you hit Ctrl+C, the server waits for that specific request to finish before the process exits.
package main

import (
	"fmt"
	"net/http"
	"time"
)

func paymentHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Println("Processing payment... please wait.")
	time.Sleep(5 * time.Second) // Simulate heavy work
	fmt.Fprintln(w, "Payment processed successfully!")
	fmt.Println("Payment complete.")
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/process-payment", paymentHandler)

	srv := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}

	fmt.Println("Payment Server running on :8080...")
	// TODO: Implement the graceful shutdown logic here
	// Replace this blocking call with your signal handling and shutdown logic
	srv.ListenAndServe() 
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.