Skip to content

Health checks

Every container can tell your host whether it is working, so a broken release never takes over and a stuck one is restarted.

Container Path Healthy when
api /api/health 200; 503 if Postgres or Redis is down
web, site /healthz 200, answered by Caddy without the API
worker none It has no address. Restart it on exit

On Railway these are already set, and production:up on your machine waits for them too. On another host, point its health check at the paths above.

/api/health needs no session. It runs select 1 against the database and pings Redis on every call:

{
"status": "ok",
"version": "1.0.0",
"uptimeSeconds": 4211.3,
"checks": { "database": "up", "redis": "up" },
"timestamp": "2026-10-01T09:12:44.120Z"
}

When the database or Redis cannot be reached, status is degraded, its check is down, the answer is 503, and the reason is logged as an error. The shape is healthSchema in packages/contracts/src/health.ts, which the web app’s status card, the mobile app and deploy:check all read.

To check something else, such as a service your product cannot work without, add it to checks in the schema and to HealthService in apps/api/src/health. Keep it fast: hosts ask every few seconds, which is also why these requests are left out of the logs.

Point an uptime monitor at https://<your web address>/api/health and alert on anything but 200. It goes through Caddy to the API to the database, so one address covers all three.

After a release, or when something seems off, check everything a visitor depends on:

Terminal window
bun run deploy:check https://app.example.com --site https://example.com

See Check a deployment for what it looks at.

When your host stops a container, the API finishes what it is doing first: the worker takes no new jobs and waits for the ones in progress, then the database connections are closed. A job that was cut off anyway is picked up again after 10 minutes. See Background jobs.

On Railway a service that exits is restarted, up to 5 times in a row.