Go Gin Graceful Shutdown for Long Running Requests
Quick answer
To shut down a Gin application gracefully, run the server with http.Server, listen for SIGTERM and SIGINT, and call Shutdown with a timeout context. Shutdown...
To shut down a Gin application gracefully, run the server with http.Server, listen for SIGTERM and SIGINT, and call Shutdown with a timeout context. Shutdown stops accepting new connections and waits for in-flight requests to finish. Long running work needs extra care, because Shutdown does not cancel handlers that are still executing.
How Do You Implement Graceful Shutdown in Gin?
Gin's router.Run starts a server you cannot stop cleanly, so use the standard library's http.Server with Gin as the handler. Start the server in a goroutine, wait for an OS signal, then call Shutdown with a deadline.
srv := &http.Server{Addr: ":8080", Handler: router}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %v", err)
}
}()
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("forced shutdown: %v", err)
}
ErrServerClosed is the expected result after Shutdown, so it should not be treated as a failure. Make sure your shutdown timeout is shorter than the grace period your platform gives the process, otherwise the process is killed before your cleanup finishes.
Containers make this especially important. Orchestrators usually send SIGTERM first and then SIGKILL after a configured grace period. If your process ignores SIGTERM, every deploy becomes a hard kill, and any request in flight at that moment is dropped without a response.
What Happens to Long Running Requests During Shutdown?
Shutdown closes listeners, then waits for active connections to become idle. It does not interrupt handlers. A request that takes two minutes will block shutdown until it finishes or until your context deadline expires, at which point Shutdown returns an error and the process typically exits anyway.
This means you need a policy. Decide the maximum duration you are willing to wait, then design handlers to respect cancellation. Use c.Request.Context() for database calls and outbound HTTP requests so work stops when the client disconnects. Note that Shutdown itself does not cancel request contexts, so use a base context on the server if you want shutdown to signal handlers.
baseCtx, cancelBase := context.WithCancel(context.Background())
srv := &http.Server{
Addr: ":8080",
Handler: router,
BaseContext: func(net.Listener) context.Context { return baseCtx },
}
// after the grace period expires: cancelBase()
Log what happens during shutdown. Record when the signal arrived, how many requests were still active, and whether the drain finished cleanly or timed out. Those few log lines make it much easier to tune the grace period later based on real behavior instead of guesses.
How Should You Handle Background Work and Jobs?
Goroutines started from handlers are not tracked by the HTTP server. If a handler spawns background work, Shutdown can return while that work is still running, and the process may exit and lose it.
- Track background goroutines with a sync.WaitGroup and wait for it after Shutdown returns.
- Prefer a durable queue for work that must not be lost, instead of fire-and-forget goroutines.
- Close database pools and message producers after the server has drained, not before.
- Make long operations resumable or idempotent so a forced stop is safe.
For uploads, streaming responses, and WebSockets, decide explicitly how they should end. Hijacked connections such as WebSockets are not tracked by Shutdown, so notify clients and close them yourself. Server-sent event streams can run indefinitely, so they also need an explicit signal that tells them to finish.
How Does Graceful Shutdown Fit With Load Balancers?
There is a race at deploy time. The orchestrator marks an instance as terminating, but the load balancer may keep sending requests for a few seconds. If your process stops accepting connections immediately, those requests fail.
A common pattern is to fail the readiness endpoint first, wait briefly so the load balancer removes the instance, and only then call Shutdown. The right delay depends on your platform, so check its documentation for termination and endpoint propagation behavior.
Frequently Asked Questions
Does router.Run support graceful shutdown?
Not directly. It blocks and gives you no handle to stop the server. Use http.Server with Gin as its Handler so you can call Shutdown.
Does Shutdown cancel in-flight handlers?
No. It waits for them to complete. Use a base context or your own cancellation signal if handlers need to stop when the grace period ends.
What timeout should I use for Shutdown?
Slightly longer than your slowest legitimate request, but shorter than the platform's kill deadline. Many teams use 15 to 30 seconds.
Why do I still see errors during deploys?
Often the load balancer still routes traffic to the stopping instance. Fail readiness first and add a short delay before calling Shutdown.
Summary
Use http.Server, handle termination signals, drain with Shutdown and a bounded timeout, and make handlers cancellation-aware. Track background work separately and coordinate with your load balancer. Check the Go net/http documentation for details that vary by version.