Graceful Shutdown
Graceful Shutdown
Section titled “Graceful Shutdown”This guide shows how to extend the reference service’s bootstrap with OS signal handling so that in-flight requests complete before the process exits.
For the lifecycle details, see the Core Primer.
What this guide covers
Section titled “What this guide covers”- The
Prepare/Server/Shutdownlifecycle sequence - Capturing
SIGTERMandSIGINTwithsignal.NotifyContext - Waiting for in-flight requests to drain before exiting
- Closing application resources (DB connections, etc.) on shutdown
The canonical lifecycle
Section titled “The canonical lifecycle”New(cfg) → wires deps, attaches middlewareRegisterRoutes() → attaches handlers to the routerPrepare() → builds the internal server handleServer() → returns *http.Server ready to accept connectionsListenAndServe() → blocks until the server stopsShutdown(ctx) → drains connections, then returnscore.App does not handle OS signals itself — that is the responsibility of main or the Start method in your application layer.
Step 1 — Replace the blocking Start with a signal-aware version
Section titled “Step 1 — Replace the blocking Start with a signal-aware version”The reference service’s Start method blocks in ListenAndServe. Replace it with a version that listens for signals in parallel:
import ( "context" "fmt" "os/signal" "syscall" "time")
func (a *App) Start() error { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer stop()
if err := a.Core.Prepare(); err != nil { return fmt.Errorf("prepare: %w", err) } srv, err := a.Core.Server() if err != nil { return fmt.Errorf("get server: %w", err) }
serverErr := make(chan error, 1) go func() { serverErr <- srv.ListenAndServe() }()
select { case err := <-serverErr: return fmt.Errorf("server stopped: %w", err) case <-ctx.Done(): // Signal received — begin graceful drain. }
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil { return fmt.Errorf("shutdown: %w", err) } a.Core.Shutdown(shutdownCtx) return nil}Step 2 — Close resources after the server drains
Section titled “Step 2 — Close resources after the server drains”If your application holds a database connection, a cache client, or file handles, close them after srv.Shutdown returns:
a.Core.Shutdown(shutdownCtx)if a.DB != nil { _ = a.DB.Close()}return nilsrv.Shutdown waits for all active connections to complete. Resources that those handlers depend on must stay open until shutdown returns.
Step 3 — Verify the timeout
Section titled “Step 3 — Verify the timeout”15 seconds is a common drain timeout, but tune it to your longest expected request. Long-running stream or upload handlers may need more time. Too short a timeout causes active requests to be cut off; too long delays process replacement in your orchestrator.
Set the timeout from config so it is tunable without recompiling:
shutdownCtx, cancel := context.WithTimeout( context.Background(), time.Duration(a.Cfg.ShutdownTimeoutSec)*time.Second,)What this pattern gives you
Section titled “What this pattern gives you”ListenAndServeerrors (port already in use, TLS failures) surface immediately via theserverErrchannel.signal.NotifyContextcancels automatically onSIGTERMorSIGINT— no manual signal channel setup.- The drain timeout is explicit and configurable rather than implicit in the process kill timeout.
- Resources are closed in dependency order: server first, then anything handlers depend on.
Complete example in the reference app
Section titled “Complete example in the reference app”reference/standard-service/main.go shows the complete graceful-shutdown wiring — signal context, ListenAndServe error channel, and app.Shutdown with a deadline — in a runnable service.
Read next
Section titled “Read next”- core API Reference —
App.Prepare,App.Server, andApp.Shutdownsignatures - Core Primer
- Connect a Database
- Health and Readiness