---
id: platform/http-shutdown
title: Finish accepted HTTP requests during shutdown
description: Drain in-flight HTTP work on SIGTERM with readiness, keep-alive admission, and a finite application deadline under Bex's termination budget.
keywords: [bex, graceful shutdown, SIGTERM, readiness, keep-alive, SSE, maxShutdownDelaySeconds]
sidebar_position: 41
last_updated: 2026-09-23
---

Web and private services lose traffic and receive SIGTERM when a release
replaces them. An app that exits on the first signal drops requests that were
already accepted. This page points at a downloadable sample that finishes
bounded work, refuses new work while draining, and exits within a configured
deadline.

Source pin: Bex `88f0a89792d705a079075c383cf0a5a9561acc06`
(`deployment_projection.go`: 10s `preStop.sleep` for traffic-serving pods;
workers and disk-attached Recreate services get no preStop;
`maxShutdownDelaySeconds` is the post-SIGTERM budget, default 30s; pod grace =
preStop + that delay). The pin documents source behavior — not which revision
is deployed at `api.bex.co`.

## What the sample does

The [`http-shutdown`](/examples/http-shutdown/README.md) example:

1. Listens on `0.0.0.0:$PORT` with `GET /readyz` for readiness
2. Serves bounded `GET /work?ms=` requests and an SSE `GET /stream` path
3. On the first `SIGTERM`/`SIGINT`, enters `draining` once — readiness fails and
   new work (including on keep-alive sockets) returns 503
4. Lets accepted work finish inside `SHUTDOWN_DEADLINE_MS`, then exits
5. Forces unfinished streams/work when the deadline elapses

The application does **not** sleep for the platform preStop window. preStop
runs before SIGTERM so endpoints can drain; your process budget starts at the
signal.

## Run locally

```bash
curl -fsSLO https://bex.co/examples/http-shutdown-latest.zip
curl -fsSLO https://bex.co/examples/http-shutdown-latest.zip.sha256
shasum -a 256 -c http-shutdown-latest.zip.sha256
unzip http-shutdown-latest.zip
cd http-shutdown
npm test
npm run rehearse
```

Node 22+. No npm dependencies. `npm test` and `npm run rehearse` spawn a real
child process and use sockets/signals; they clean up only that child.

Local success is not evidence of Traefik endpoint propagation or whole-service
zero downtime.

## Deploy on Bex

Use the sample `render.yaml`: `type: web`, `healthCheckPath: /readyz`,
`maxShutdownDelaySeconds: 30`, and `SHUTDOWN_DEADLINE_MS` below that budget.
Docker `CMD` is exec-form (`["node","server.mjs"]`) so Node is PID 1 and
receives signals.

Workers and disk-backed Recreate services skip the 10s preStop drain at the
pinned revision — they still get SIGTERM and the configured shutdown delay.

## Troubleshooting

| Symptom | Check |
| --- | --- |
| Readiness never passes | `HOST`/`PORT`, process listening on `0.0.0.0`, probe path `/readyz` |
| Requests cut off on deploy | Signal reaches the app (exec-form entrypoint); drain budget vs work duration |
| Process never exits | `SHUTDOWN_DEADLINE_MS` must bound streams; avoid ignoring SIGTERM |
| Still serving after SIGTERM | Expected briefly for in-flight work; new work must 503 while `draining` |

## Related

- [How deploys work](./how-deploys-work.md)
- [Health checks](./health-checks.md)
- [Web services](./web-services.md)
- [Docker deploys](./docker-deploys.md)
