Gracefully Shutting Down a Server in Go

Gracefully Shutting Down a Server in Go

The post covers how to gracefully shut down a server in Go. This is important as there may be some tooling you're using that is batching things locally i.e. traces, logs, events, etc. behind the scenes and if the server shutdowns, we may lose this data. What we need to do is to listen to these shutdown signals and run any clean up tasks needed before we take things down.

This is a pretty common practice and definitely something you'll want to be aware of if you're using Kubernetes as pods can start up and shut down for a lot of reasons like new deployments, scaling down after a traffic spike, draining a node, etc.

Basic Server Example

The code below starts up a pretty basic server in Go.

package main

import (
	"log"
	"net/http"
)

func main() {

	// Configure server
	log.Println("configuring server ...")
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		log.Println("received call")
	})
	server := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}

	// Start server
	log.Println("starting server ...")
	if err := server.ListenAndServe(); err != http.ErrServerClosed {
		log.Println("an error occurred running the server")
	}

	// Clean up
	log.Println("cleaning up before shutdown")
}

When we use Control+C to kill the server, we aren't able to get to the clean up code at the bottom of main. The output looks like this:

2021/05/28 01:09:06 configuring server ...
2021/05/28 01:09:06 starting server ...
^Csignal: interrupt

Handling Signals

What we'd like to do is capture this signal and run the clean up tasks before shutting down. We can do this by creating a channel and relaying the specified signals to this channel like so:

signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
SIGINT is sent when Control+C is used. SIGTERM is sent by Kubernetes to initiate a shutdown.

Now we have to add some code that waits for these signals, we can do that at the bottom of the main function like this:

_ = <-signals

In order for this to work, we need to start up our server in a Go routine, so that startup of the server doesn't block up the execution. This way our server will wait on the line above to receive a signal at that channel.  

The resulting code would look like this:

package main

import (
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
)

func main() {

	// Configure channels
	log.Println("configuring channel ...")
	signals := make(chan os.Signal, 1)
	signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)

	// Configure server
	log.Println("configuring server ...")
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		log.Println("received call")
	})
	server := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}

	// Start server
	go func() {
		log.Println("starting server ...")
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			log.Printf("an error occurred running the server")
		}
	}()

	// Wait for signals
	_ = <-signals
	log.Println("shutting down server ...")
	log.Println("server shutdown")
}

Now, when we use Control+C, our server shutdown code will execute like we wanted it to:

2021/05/28 01:32:01 configuring channel ...
2021/05/28 01:32:01 configuring server ...
2021/05/28 01:32:01 starting server ...
^C2021/05/28 01:32:02 shutting down server ...
2021/05/28 01:32:02 server shutdown