Test Graceful Shutdown Signal Handling
Assert that a service stops accepting new work, drains in-flight requests, and exits within the shutdown deadline on SIGTERM.
Task
Write an integration test that sends SIGTERM and verifies draining behaviour.
When to use this
Deployments cut off in-flight requests because the process exits immediately on SIGTERM instead of draining.
Prerequisites
- A service that handles SIGTERM
- A test harness that can send signals
- A slow endpoint to observe draining
Steps
1. Start the service and issue a slow request.
const child = spawn('node', ['server.js']);
const slow = fetch('http://localhost:3000/slow');
await new Promise(r => setTimeout(r, 200));Expected output: A request is in flight when the shutdown begins.
2. Send SIGTERM and assert the request completes.
child.kill('SIGTERM');
const res = await slow;
expect(res.status).toBe(200);
await once(child, 'exit');Expected output: The in-flight request finishes and the process exits, proving graceful draining.
Troubleshooting
Problem
Process exits before draining
Solution
Register the signal handler early and stop the listener before draining.
Problem
Shutdown hangs forever
Solution
Add a hard deadline timer that forces exit after the grace period.
Limitations
- Does not cover load-balancer connection draining.
- Signal semantics differ between platforms.
- Requires the slow endpoint to expose the behaviour.
What this recipe does
Write an integration test that sends SIGTERM and verifies draining behaviour.
Deployments cut off in-flight requests because the process exits immediately on SIGTERM instead of draining.
Prerequisites
- A service that handles SIGTERM
- A test harness that can send signals
- A slow endpoint to observe draining
Procedure
- Start the service and issue a slow request.
const child = spawn('node', ['server.js']);
const slow = fetch('http://localhost:3000/slow');
await new Promise(r => setTimeout(r, 200));
Expected result: A request is in flight when the shutdown begins.
- Send SIGTERM and assert the request completes.
child.kill('SIGTERM');
const res = await slow;
expect(res.status).toBe(200);
await once(child, 'exit');
Expected result: The in-flight request finishes and the process exits, proving graceful draining.
How to confirm it worked
Run the commands above and confirm every expected result in order. If a step does not produce the stated output, use the troubleshooting notes before continuing. A passing run shows the in-flight request completes with 200 and the process exits within the configured grace period.
Troubleshooting
Process exits before draining — Register the signal handler early and stop the listener before draining.
Shutdown hangs forever — Add a hard deadline timer that forces exit after the grace period.
Limitations
- Does not cover load-balancer connection draining.
- Signal semantics differ between platforms.
- Requires the slow endpoint to expose the behaviour.