Skip to content

Graceful Shutdown

When your process receives SIGTERM (deploy, restart, scale-down), you need to:

  1. Stop claiming new jobs
  2. Let the current handler finish
  3. Report the result back to Chronos
  4. Then exit

If you kill the process mid-handler, the execution’s lease expires on the server and Chronos schedules a retry, wasting time and potentially causing side effects if the handler wasn’t idempotent.

The SDK doesn’t install SIGTERM/SIGINT handlers because your application owns its process lifecycle. You might:

  • Run multiple services in one process
  • Need cleanup beyond the Chronos worker (close DB connections, flush logs)
  • Use a framework that manages shutdown itself

Signal handling is your responsibility.

worker.ts
import { Chronos } from '@chronos.sh/sdk';
const chronos = new Chronos({
apiKey: process.env.CHRONOS_API_KEY!,
});
chronos.worker
.handle('sync-tenant', syncTenantHandler)
.handle('send-report', sendReportHandler);
const shutdown = async () => {
await chronos.worker.stop();
process.exit(0);
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
await chronos.worker.start();
  1. Aborts the long-poll immediately: the current HTTP request to /v1/worker/jobs/claim is cancelled, so no new job is claimed
  2. Waits for in-flight work: if a handler is mid-execution, stop() waits until it finishes and reports its result
  3. Resolves: the promise returned by start() resolves, your shutdown handler continues

stop() never throws. If no work is in-flight, it resolves immediately.

If a handler is stuck (infinite loop, deadlock), the first SIGTERM will wait forever. Handle a second signal as a force-exit:

let stopping = false;
const shutdown = async () => {
if (stopping) {
process.exit(1);
}
stopping = true;
await chronos.worker.stop();
process.exit(0);
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

First signal: graceful. Second signal: immediate exit. The abandoned execution will time out on the server and be retried.

The longest a graceful shutdown can take:

handler runtime + result report retries

Result reporting retries up to 3 times with retryDelayMs (default 1000ms) between attempts. Worst case:

handler runtime + (3 × 1000ms) ≈ handler runtime + 3 seconds

Set your process manager’s grace period to exceed your longest expected handler runtime plus a few seconds.

If your process has more to clean up beyond the worker:

const shutdown = async () => {
// Stop accepting new work
await chronos.worker.stop();
// Clean up other resources
await db.disconnect();
await redis.quit();
await logger.flush();
process.exit(0);
};

Containers receive SIGTERM on shutdown. Ensure your STOPSIGNAL is SIGTERM (the default) and your grace period covers your longest handler:

Dockerfile
# Default: STOPSIGNAL SIGTERM (no change needed)
STOPSIGNAL SIGTERM

For Docker Compose or Swarm, set the stop grace period:

docker-compose.yml
services:
worker:
stop_grace_period: 60s # Match to your longest handler + buffer

For other orchestrators (ECS, systemd, Caprover), configure the equivalent grace period to exceed your longest expected handler runtime.

Verify graceful shutdown works by sending SIGTERM while a handler is running:

Terminal window
# Terminal 1: start your worker
CHRONOS_API_KEY=chrns_... npx tsx worker.ts
# Terminal 2: send SIGTERM
kill -TERM $(pgrep -f worker.ts)

Watch for the handler to complete and the process to exit cleanly. If it exits immediately (code 137/143 without completing), your signal wiring isn’t working.