Deployment · beginner
How to verify graceful shutdown under in-flight requests
Verification guide for confirming that a service performs graceful shutdown under in-flight requests. Walks through observable signals, an ordered ten-minute triage, boundary-specific inspection points, diagnostic steps that separate SIGTERM misconfiguration from listener and hook defects, conditional safe fixes, and a regression check that re-proves the policy under load.
The symptoms
- •Active HTTP connections are reset with ECONNRESET or RST during a rolling restart, surfacing 5xx spikes correlated to deploy events.
- •Long-running requests (uploads, streaming responses, websocket upgrades) are aborted mid-flight rather than allowed to complete within a shutdown window.
- •Process exits before the configured shutdown grace window elapses, indicating the signal handler is missing or the event loop is being torn down prematurely.
- •New requests continue to be accepted after SIGTERM, contradicting the "drain" phase of a graceful shutdown policy.
- •Deployment system reports the pod as "ready" again before in-flight work finishes, hiding unfinished requests behind a healthy probe.
Likely causes
- •The process does not register a handler for SIGTERM (or platform equivalent), so the runtime default is immediate termination once the orchestrator sends the termination signal.
- •A graceful shutdown hook exists but does not call server.close() or equivalent, so existing keep-alive sockets are not asked to finish their current request.
- •Shutdown grace period is shorter than realistic request completion time, so requests longer than the window are forcibly killed by the orchestrator's terminationGracePeriodSeconds.
- •Readiness probe is not gated on in-flight drain, so the load balancer shifts traffic to a replacement instance before draining completes.
- •Keep-alive sockets from upstream proxies keep the listener busy, preventing server.close() callback from firing within the budget.
- •Worker threads or background queues ignore the shutdown signal and continue to dispatch work to the request path after the listener has been told to stop.
First ten minutes
- 01Confirm the failure surface: identify whether 5xx errors, connection resets, or premature exit codes correlate with a deploy or restart event in the orchestration timeline.
- 02Locate the process signal contract: read the deployment manifest entry for terminationGracePeriodSeconds, preStop hook, and the container's PID 1 command, and compare it to the shutdown handler documented in the application.
- 03Capture the last shutdown log lines: identify the timestamp of SIGTERM reception, the shutdown hook entry, the listener close invocation, and the process exit code, in that order.
- 04Quantify in-flight work at the moment of shutdown: record the number of active requests, their elapsed time, and the longest-pending connection, so the policy window can be evaluated against real load.
- 05Decide which layer failed first: signal handling, listener drain, hook ordering, or orchestrator grace period, before touching configuration.
Evidence to collect
- •Orchestration event log entries pairing the termination signal timestamp with the container's reported exit code and reason.
- •Application shutdown sequence logs marking SIGTERM reception, server.close() invocation, the "drained" callback, and the subsequent process.exit() call, with their deltas.
- •Active request count and request elapsed-time histogram captured at the SIGTERM timestamp, plus the count of requests that did not produce a 2xx response.
- •Listener state showing new connection attempts accepted vs rejected during the drain window, and the time from SIGTERM to "no active sockets".
- •Reverse proxy or load balancer access log slice spanning SIGTERM ± grace period, showing client-side connection resets and response status codes.
- •Process tree and PID 1 identification proving whether a signal trap is in place at the top-level process or only inside a child.
Where to look
- •The deployment manifest boundary: terminationGracePeriodSeconds, preStop lifecycle hook, and the container command that becomes PID 1.
- •The process signal boundary: the PID 1 process and its signal handlers, and any init wrapper that may swallow SIGTERM before the application sees it.
- •The HTTP listener boundary: server.close() / equivalent drain callback, keep-alive socket state, and the readiness probe endpoint.
- •The request handling boundary: in-flight request registry or middleware that tracks active handlers and exposes the drain completion condition.
- •The upstream boundary: reverse proxy, load balancer, and service mesh sidecar configuration for connection drain and endpoint removal timing.
- •The shutdown hook boundary: registered beforeExit, SIGTERM listeners, and the explicit ordering of "stop accepting" → "wait for in-flight" → "close resources" → "exit".
Diagnostic steps
- 01Verify signal delivery: confirm SIGTERM is reaching the application process by comparing orchestration termination timestamps with the application's "received signal" log line; absence indicates an init wrapper or PID misassignment is swallowing the signal.
- 02Verify drain invocation: confirm server.close() (or platform equivalent) is called inside the SIGTERM handler; if missing, the listener continues to accept sockets and the policy is not being enforced.
- 03Verify drain completion condition: confirm the handler waits for the server.close() callback (or for an explicit in-flight counter to reach zero) before calling process.exit(); exiting on a timer alone violates the bounded policy.
- 04Verify grace window sizing: compare the longest observed request duration against terminationGracePeriodSeconds; a window shorter than p99 in-flight duration will force-kill legitimate work.
- 05Verify readiness gating: confirm the readiness probe flips to "not ready" before SIGTERM is sent (preStop hook) so the load balancer stops routing new traffic; otherwise new requests keep arriving during the drain.
- 06Verify keep-alive drain: confirm idle keep-alive connections are closed by the server during drain; lingering idle sockets can keep the close() callback pending past the grace window.
- 07Verify child process compliance: if workers or background queues exist, confirm they also honor the shutdown signal; uncoordinated children can hold the event loop open or write responses after the listener is closed.
- 08Distinguish orchestrator kill from application bug: a SIGKILL after grace expiry implies the application did not exit in time, which is a policy-length or hook-ordering defect, not a signal-delivery defect.
Common mistakes
- •Treating any single clean shutdown under no load as proof of graceful shutdown; the policy must hold under in-flight requests, not only when the listener is idle.
- •Increasing terminationGracePeriodSeconds without first verifying the application actually drains, which masks the real defect behind a longer wall clock.
- •Relying on process.exit() inside the SIGTERM handler without calling server.close() first, which terminates the event loop before in-flight responses can flush.
- •Letting the readiness probe remain green until the moment the container terminates, so the load balancer keeps sending new requests into a draining instance.
- •Wrapping the application in an init process that does not forward SIGTERM, leaving the application unaware that shutdown has begun.
- •Closing the listener but not draining keep-alive sockets, so server.close() never calls back within the grace window and the orchestrator escalates to SIGKILL.
Safe fixes
- •If SIGTERM is not reaching the application: ensure the container command runs the application as PID 1 directly, or use an init wrapper that forwards SIGTERM to the child, and re-verify the "received signal" log line appears.
- •If the drain is missing: register a SIGTERM handler that calls server.close() (or equivalent), then awaits the close callback or an in-flight counter reaching zero, and only then calls process.exit(); re-verify the sequence in logs.
- •If the grace window is too short: raise terminationGracePeriodSeconds only after confirming p99 in-flight request duration plus a safety margin is below the new window; re-verify with a load-injected shutdown test.
- •If readiness is not gated: add a preStop hook that flips readiness to "not ready" and sleeps briefly before SIGTERM, so the load balancer stops routing; re-verify by observing new-connection counts drop to zero before drain completes.
- •If keep-alive sockets linger: close idle keep-alive sockets explicitly during the drain phase so server.close() can complete; re-verify the close callback fires within the budget.
- •If background workers ignore shutdown: propagate the shutdown signal to workers or queue drainers and wait for their quiescence before exiting; re-verify by checking worker shutdown logs align with the parent drain.
Prove the fix
- 01During a forced restart with synthetic load, observe zero new 5xx responses and zero connection resets on requests that were in-flight before SIGTERM, and confirm all such requests return their normal 2xx response.
- 02Observe the application logs in order: SIGTERM received → server.close() called → "in-flight count: N" → "in-flight count: 0" → process.exit(0), with the drain interval within the configured grace window.
- 03Observe the readiness probe transition to "not ready" before SIGTERM and the load balancer stop sending new connections before the drain begins, verified via upstream access logs.
- 04Repeat the verification after any change to the shutdown handler, signal wrapper, deployment manifest, or upstream drain configuration, and treat a single failed run as a regression.
Prevention and next steps
- •Treat graceful shutdown as a tested contract: include a load-injected shutdown test in CI that asserts no in-flight request is reset and the process exits within the grace window.
- •Keep the shutdown sequence explicit and ordered in code: stop accepting, await in-flight zero, close resources, exit; document each step and its corresponding log line.
- •Right-size terminationGracePeriodSeconds against measured p99 in-flight duration plus a safety margin, and revisit whenever request latency profiles change.
- •Gate readiness on drain readiness, not on process liveness, so the load balancer removes the endpoint before SIGTERM and adds it back only after a clean start.
- •Avoid init wrappers or supervisors that swallow SIGTERM, and verify signal forwarding with an explicit log entry on every restart.
Safe commands and checks
ps -o pid,ppid,stat,cmd -p <pid> # confirm <pid> is the application PID 1 and identify any parent init wrapper that may intercept SIGTERM.
kill -TERM <pid> # deliver SIGTERM manually to a non-production instance to observe the shutdown sequence without a deploy.
ss -tan state established '( sport = :<port> )' # enumerate active established sockets on <port> at the moment of SIGTERM to measure in-flight load.
kill -0 <pid> # probe liveness of <pid> during the drain window without sending a signal, to confirm the process is still draining rather than already exited.
grep -nE 'SIGTERM|shutdown|drain|close|in[-_ ]flight' <logfile> # extract the ordered shutdown log lines from <logfile> for the most recent restart.
awk 'NR==FNR{ts[$1]=1; next} {if(ts[$1]) print}' <sigterm_timestamps> <access_log> # correlate upstream access log entries with SIGTERM timestamps from <sigterm_timestamps> to detect resets during the drain.