Cloud services are stopped routinely: deployments replace instances, autoscalers remove capacity, hosts reboot, and schedulers reschedule workloads. If a process exits immediately when it receives a termination signal, in-flight requests can fail even when the service is otherwise healthy.

Graceful shutdown is a small lifecycle protocol: stop taking new work, allow useful work already in progress to finish within a deadline, then release resources and exit.

Separate readiness from process health

A process that is shutting down may still be alive but should no longer receive new traffic.

That means readiness and liveness answer different questions:

  • readiness: should the load balancer send new work here?
  • liveness: is the process functioning enough to remain running?

During shutdown, mark the instance unready first. Keep it alive long enough to finish accepted requests.

If the process exits before traffic routing reacts, clients can continue sending requests to an endpoint that disappears mid-flight.

Stop accepting new work first

A typical shutdown sequence is:

termination signal
  -> readiness false
  -> stop new accepts
  -> wait for active work
  -> close dependencies
  -> process exits

For an HTTP server, “stop new accepts” may mean closing the listening socket while keeping established connections alive long enough to complete current requests.

For a worker, it may mean pausing queue consumption before finishing jobs already reserved.

The exact mechanism differs, but the ordering is the same: reduce incoming work before waiting for current work to disappear.

Put a deadline on draining

Graceful shutdown must be bounded. A request can hang, a dependency can stop responding, or a client can hold a connection open indefinitely.

Choose a shutdown budget that fits the platform’s termination grace period:

platform grace period: 30 s
routing propagation:    5 s
request drain budget:  20 s
cleanup reserve:        5 s

The numbers are workload-specific. The important part is leaving room for final cleanup before the platform sends a hard kill.

Requests should already have their own deadlines. Shutdown is not a reason to let abandoned work run forever.

Drain long-lived connections deliberately

WebSockets, streaming HTTP responses, and other long-lived connections do not naturally finish within a short deployment window.

A service may need to:

  • stop accepting new streams;
  • tell clients to reconnect;
  • close connections with an application-specific reason;
  • let load-balancer connection draining handle established flows;
  • enforce a maximum age or shutdown deadline.

Without a policy, a single long-lived connection can prevent graceful termination indefinitely.

Coordinate with load balancers

Application shutdown and external traffic routing are separate systems.

Even after readiness changes, load balancers, service discovery, proxies, or sidecars may need time to observe the update. Build a small propagation allowance into the lifecycle when the environment requires it.

Do not use a fixed sleep as the only safety mechanism if the platform exposes a stronger readiness or deregistration signal. Sleeps are easy to mis-size and add delay even when routing has already converged.

Treat background work as part of shutdown

HTTP requests are not the only in-flight operations. Services often run:

  • queue consumers;
  • scheduled tasks;
  • batch flushers;
  • telemetry exporters;
  • cache refreshes;
  • asynchronous writes.

Each component should define whether it should finish, checkpoint, cancel, or hand off work during termination.

A background goroutine, thread, or task that keeps starting new work after the server begins draining can prevent a clean exit.

Close resources after useful work stops

Database pools, message clients, files, and telemetry exporters should generally stay available while in-flight requests still need them.

Closing a database pool before request draining completes creates failures inside work that the service intended to preserve.

Cleanup ordering should follow dependencies:

stop producers
finish consumers
flush final telemetry
close shared clients
exit

Make handlers cancellation-aware

When the shutdown deadline expires, remaining work needs a cancellation signal.

Handlers that ignore cancellation can continue consuming CPU or waiting on dependencies until the process is forcibly killed. Propagate cancellation to database calls, downstream HTTP requests, and other operations that support it.

A graceful system therefore needs both completion and cancellation paths.

Test termination under load

A server that shuts down correctly while idle can still fail during a deployment with active traffic.

A useful test is:

  1. run sustained representative requests;
  2. trigger the normal termination signal;
  3. confirm the instance becomes unready;
  4. verify new work moves elsewhere;
  5. measure how many accepted requests finish;
  6. confirm the process exits before the hard deadline.

Repeat with slow requests and long-lived connections.

Observe the shutdown lifecycle

Useful signals include:

  • shutdown start and completion timestamps;
  • active request count when draining begins;
  • requests completed during drain;
  • requests canceled at the deadline;
  • open connections;
  • queue jobs still active;
  • forced termination count;
  • shutdown duration percentiles.

A deployment that regularly reaches the hard termination limit is not truly graceful, even if most requests happen to survive.

Common pitfalls

Exiting immediately on SIGTERM

Termination signals should initiate the lifecycle, not bypass it.

Staying ready while draining

If new requests keep arriving, the active count may never reach zero.

Closing shared clients too early

Resources needed by in-flight requests should remain available until those requests finish or are canceled.

Waiting forever

Every drain needs a maximum duration and a forced-cancellation path.

Testing only with short HTTP requests

Streaming connections, workers, and slow dependencies often expose shutdown bugs first.

Make shutdown a normal operating path

Graceful termination should be exercised on every ordinary deployment, not reserved for rare incidents. That makes lifecycle bugs visible when the system is healthy and gives operators confidence that scaling down does not create avoidable errors.

Cloud infrastructure will stop processes eventually. Reliable services cooperate with that fact by withdrawing from traffic, draining useful work within a bounded deadline, and releasing dependencies in a deliberate order.