golang
graceful shutdown
web server
go programming
server management

Graceful shutdown of golang web server

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

A graceful shutdown lets a Go web server stop accepting new requests while allowing in-flight requests to finish within a deadline. This is important in production because abrupt termination can drop active connections, interrupt writes, and leave background work half-finished.

In Go's net/http package, the core tool for this is http.Server.Shutdown, usually combined with signal handling and a timeout context.

Why Server.Close Is Not Enough

Server.Close stops the server immediately by closing active listeners and connections. That is sometimes acceptable for local tools, but it is not graceful.

For graceful behavior, use:

  • 'ListenAndServe to run the server'
  • signal handling for SIGINT or SIGTERM
  • 'Shutdown to stop cleanly with a deadline'

Shutdown tells the server to reject new connections and wait for existing handlers to return.

A Minimal Graceful Shutdown Example

go
1package main
2
3import (
4    "context"
5    "log"
6    "net/http"
7    "os"
8    "os/signal"
9    "syscall"
10    "time"
11)
12
13func main() {
14    mux := http.NewServeMux()
15    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
16        time.Sleep(2 * time.Second)
17        w.Write([]byte("done"))
18    })
19
20    srv := &http.Server{
21        Addr:    ":8080",
22        Handler: mux,
23    }
24
25    go func() {
26        log.Println("server started on :8080")
27        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
28            log.Fatalf("listen error: %v", err)
29        }
30    }()
31
32    stop, err := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
33    if err != nil {
34        log.Fatalf("signal setup error: %v", err)
35    }
36    defer stop()
37
38    <-stop.Done()
39    log.Println("shutdown signal received")
40
41    shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
42    defer cancel()
43
44    if err := srv.Shutdown(shutdownCtx); err != nil {
45        log.Printf("graceful shutdown failed: %v", err)
46        if closeErr := srv.Close(); closeErr != nil {
47            log.Printf("forced close failed: %v", closeErr)
48        }
49    }
50
51    log.Println("server stopped")
52}

This is the standard shape for graceful shutdown in a Go HTTP service.

What Shutdown Actually Does

When Shutdown is called:

  • listeners stop accepting new connections
  • idle connections are closed
  • active handlers are allowed to complete until the context expires

If the timeout is reached, Shutdown returns an error. At that point you may choose to force Close.

This is why the timeout matters. Without it, shutdown could hang indefinitely if handlers never return.

Handling Background Work

Graceful shutdown is not only about HTTP handlers. Many services also run:

  • worker goroutines
  • message consumers
  • database flush loops
  • telemetry exporters

Those components should also listen for cancellation and exit cleanly. A common pattern is to share a root context or a stop channel with background workers.

Example worker:

go
1func worker(ctx context.Context) {
2    for {
3        select {
4        case <-ctx.Done():
5            log.Println("worker stopping")
6            return
7        default:
8            time.Sleep(500 * time.Millisecond)
9        }
10    }
11}

If the HTTP server shuts down gracefully but background goroutines ignore cancellation, the process may still hang or exit uncleanly.

Choosing a Timeout

The timeout should be long enough for normal requests to finish, but short enough that deployments and restarts do not stall forever.

A common production choice is somewhere between a few seconds and a few tens of seconds, depending on:

  • request duration
  • database transaction patterns
  • load balancer drain time
  • background cleanup needs

There is no single perfect value. It should reflect real request behavior.

Common Pitfalls

The biggest pitfall is calling Shutdown without running the server in a separate goroutine. If ListenAndServe blocks the main goroutine, the shutdown signal handling never gets a chance to run.

Another issue is forgetting the timeout context. Without a deadline, a stuck handler can prevent shutdown forever.

Developers also sometimes assume Shutdown stops background goroutines automatically. It does not. Only the HTTP server lifecycle is managed unless you wire cancellation into the rest of the program.

Finally, do not treat http.ErrServerClosed as a fatal startup failure. It is the expected return from ListenAndServe after a normal shutdown begins.

Summary

  • Use http.Server.Shutdown for graceful Go web server shutdown.
  • Handle OS signals and trigger shutdown from a separate control path.
  • Always use a timeout context so shutdown cannot hang forever.
  • Stop background goroutines explicitly; Shutdown only manages the HTTP server.
  • Reserve Server.Close as a forced fallback when graceful shutdown times out.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.