Graceful Shutdown
The problem
Section titled “The problem”When your process receives SIGTERM (deploy, restart, scale-down), you need to:
- Stop claiming new jobs
- Let the current handler finish
- Report the result back to Chronos
- 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.
Why the SDK doesn’t handle signals
Section titled “Why the SDK doesn’t handle signals”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.
Basic shutdown
Section titled “Basic shutdown”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();What stop() does
Section titled “What stop() does”- Aborts the long-poll immediately: the current HTTP request to
/v1/workers/jobs/claimis cancelled, so no new job is claimed - Waits for in-flight work: if a handler is mid-execution,
stop()waits until it finishes (or hits itstimeout) and reports its result. A handler that exceeds itstimeoutis abandoned:ctx.signalfires, the SDK reportstimeout, and it does not wait for the handler to return - Resolves: the promise returned by
start()resolves, your shutdown handler continues
stop() never throws synchronously and always resolves: immediately with no work in flight, otherwise once the in-flight handler has reported. A credential rejection (401 unauthorized) surfaces on the promise returned by start(), so the shutdown handlers above need no error handling.
Second-signal escape hatch
Section titled “Second-signal escape hatch”A handler that blocks the event loop (a synchronous infinite loop) never reaches its timeout, and a handler with a long timeout can keep the first SIGTERM waiting for minutes. 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.
Worst-case shutdown time
Section titled “Worst-case shutdown time”The longest a graceful shutdown can take:
remaining handler time + result report attemptsOnce stop() is called, retry waits are skipped, so what remains is bounded by the handler’s timeout and by timeoutMs (default 30000ms) on each of up to 3 result-report attempts. Worst case at the defaults:
handler timeout + (3 × 30000ms) = handler timeout + 90 secondsThe 90 seconds is the pathological case (the API unreachable and every request stalling to its deadline). In normal operation the result report returns in well under a second. Set your process manager’s grace period to at least your longest handler timeout plus 3 × timeoutMs; lower timeoutMs if that budget is too long for your platform.
With other cleanup
Section titled “With other cleanup”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);};Docker / container deployments
Section titled “Docker / container deployments”Containers receive SIGTERM on shutdown. Ensure your STOPSIGNAL is SIGTERM (the default) and your grace period covers the budget above:
# Default: STOPSIGNAL SIGTERM (no change needed)STOPSIGNAL SIGTERMFor Docker Compose or Swarm, set the stop grace period:
services: worker: stop_grace_period: 120s # 30s handler timeout (Free cap) + 3 × 30s timeoutMsFor other orchestrators (ECS, systemd, Caprover), configure the equivalent grace period to the same budget.
Testing shutdown
Section titled “Testing shutdown”Verify graceful shutdown works by sending SIGTERM while a handler is running:
# Terminal 1: start your workerCHRONOS_API_KEY=chrns_... npx tsx worker.ts
# Terminal 2: send SIGTERMkill -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.