Golang HTTP Server Shutdown Hangs: Troubleshooting Guide
Last updated on

Golang HTTP Server Shutdown Hangs: Troubleshooting Guide


When a Go HTTP server shutdown hangs, the root cause is often not the shutdown call itself. It is usually one or more handlers, goroutines, queue consumers, or background loops that never reach a clean stop state.

That is why shutdown incidents feel confusing. The signal arrives, the shutdown code runs, and yet the process still does not exit the way you expected. In practice, the server is usually waiting for something that still thinks it should stay alive.

This guide focuses on the practical path:

  • how to separate request draining from background-work shutdown
  • what to inspect first when graceful shutdown does not finish
  • how signal handling, handler lifecycle, and worker cancellation overlap

The short version: first identify what is still active after shutdown begins, then separate draining requests from non-request background work, and finally trace whether cancellation and stop ownership are actually wired end to end.

If you want the broader Go routing view first, go to the Golang Troubleshooting Guide.


Start with the draining path

The first useful split is whether shutdown hangs because active requests are still draining, or because background work never exits.

That difference matters because the next step is completely different:

  • if requests are still draining, inspect handlers, streaming paths, and request cancellation
  • if background work is still alive, inspect workers, loops, queue consumers, and internal stop signals

Without that first split, teams often keep reading handler code while the real issue is a worker loop that ignores shutdown entirely.


What graceful shutdown actually depends on

A clean HTTP server shutdown usually depends on several things working together:

  • the process receives and handles the signal correctly
  • the server stops accepting new work
  • active requests finish or respect cancellation
  • background goroutines stop launching new work
  • queue consumers and watchers exit cleanly

If any one of those steps is vague, shutdown can hang until the timeout expires.

That is why graceful shutdown is often more of a lifecycle coordination problem than an HTTP API problem.


Common causes to check

1. Long-running handlers

One handler can keep graceful shutdown from completing.

Typical examples:

  • long polling or streaming handlers
  • handlers waiting on a slow dependency
  • request paths that start work but do not observe context cancellation

If the handler is still active and does not notice that shutdown has started, the server may keep waiting until the shutdown timeout runs out.

2. Background workers ignore cancellation

Shutdown begins, but helper goroutines keep running because they do not receive or respect a stop signal.

This is common when:

  • a worker uses context.Background() instead of a parent shutdown context
  • ticker loops have no exit path
  • queue consumers keep waiting for work even after shutdown began

From the outside, the server looks like it is stuck. In reality, some internal loop still believes it should stay alive.

3. Signal and shutdown flow are incomplete

Sometimes the process receives the signal, but the application does not close all internal loops cleanly.

Typical failure patterns:

  • the HTTP server shuts down, but internal goroutines keep running
  • one component waits for another to stop, but the ownership is unclear
  • shutdown order is inverted, so workers wait on resources that already disappeared

This is why one “graceful shutdown timeout” can hide a broader lifecycle problem.


A practical debugging order

When shutdown hangs, this order usually narrows the issue fastest:

  1. identify what remains active after shutdown starts
  2. compare request-draining paths with background-worker state
  3. inspect whether all active loops observe cancellation
  4. review signal handling and stop order between components
  5. decide whether the hang belongs in handlers, workers, or shutdown ownership

This order works because it stops you from debugging the shutdown call in isolation. The call is usually fine. The lifecycle around it is where the bug lives.

If stuck workers also look like goroutine retention, compare with Golang Goroutine Leak.


A small example of shutdown that still depends on real cooperation

go srv.ListenAndServe()

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)

This looks correct, but it only works cleanly if the rest of the system cooperates.

If handlers ignore cancellation, workers keep running, or internal loops never stop, shutdown can still hang until the timeout expires.

The key point is that srv.Shutdown coordinates shutdown. It does not magically make your own goroutines exit correctly.


A good question for every long-lived loop

For each worker, watcher, or consumer, ask:

  • who starts it
  • who owns stopping it
  • what signal tells it to exit
  • what happens if shutdown begins while it is waiting

If one of those answers is unclear, shutdown bugs become much more likely.

This framing helps because many shutdown incidents are really ownership incidents in disguise.


How shutdown hangs overlap with goroutine leaks

These two incidents often overlap.

If long-lived goroutines have no clean stop path, they can:

  • delay process exit
  • keep resources open
  • hold references in memory
  • make the shutdown timeout look like an HTTP problem

Use this quick split:

  • if the main symptom is “the server does not exit cleanly,” start with shutdown
  • if the main symptom is “many goroutines are accumulating in waiting states,” compare with goroutine leak immediately after

FAQ

Q. Does srv.Shutdown stop all goroutines automatically?

No. It helps coordinate HTTP server shutdown, but your own workers, loops, and background tasks still need explicit cancellation and stop logic.

Q. What should I inspect first during a shutdown hang?

Find what is still active after shutdown begins, then separate request handlers from non-request background work.

Q. Why does graceful shutdown still time out even when the signal path looks correct?

Because the signal path may be fine while one handler, worker, or queue consumer still has no real exit path.


Sources:

Start Here

Continue with the core guides that pull steady search traffic.