Guarantees
Chronos provides two guarantees. Understanding what they cover, and what they don’t, is essential to building a correct integration.
Exactly-once scheduling
Section titled “Exactly-once scheduling”Each schedule fires exactly one job per interval, regardless of how many instances of the Chronos scheduler are running.
How it works: When a schedule is due, the job is inserted with a composite idempotency key ({scheduleId}.{scheduledFor_ms}) using a database ON CONFLICT DO NOTHING clause. Combined with row-level locking (FOR UPDATE SKIP LOCKED) during the spawn process, this ensures that even if multiple scheduler instances race to process the same schedule, only one job is created.
What this means for you: You don’t need to worry about duplicate jobs being created from a single schedule tick. If your schedule says “every hour,” you’ll get exactly one job per hour. Not zero, not two.
What this does NOT mean: This is not exactly-once execution. A job may be executed more than once (retries, timeout + late completion, network partitions). That’s covered by the delivery guarantee below.
At-least-once delivery
Section titled “At-least-once delivery”Every job will be executed at least once. If delivery fails, Chronos retries with exponential backoff until the job succeeds or exhausts its max_retries.
How it works:
- Push delivery: Chronos makes an HTTP request. If your endpoint returns 5xx, times out, or is unreachable, the execution is marked failed and a retry is scheduled.
- Pull delivery: Your worker claims a job and runs the handler. If the handler throws, the SDK reports failure. If the worker crashes without reporting (lease expires), a background sweep detects the timeout and schedules a retry.
Retry formula: Exponential backoff with jitter, capped at 1 hour:
baseDelay = min(1000ms × 2^(attempt-1), 1 hour)jitter = random(0, baseDelay)delay = min(baseDelay + jitter, 1 hour)| Attempt | Base delay | Actual delay range |
|---|---|---|
| 1 | 1s | 1-2s |
| 2 | 2s | 2-4s |
| 3 | 4s | 4-8s |
| 4 | 8s | 8-16s |
| 5 | 16s | 16-32s |
Default max_retries is 3. Set to 0 to disable retries entirely.
What this means for you: If your handler is reachable, it will run. Transient failures (network blips, deploy windows, temporary overload) are handled automatically.
What this does NOT mean: Chronos doesn’t guarantee your handler succeeds. It only guarantees invocation. If your handler has a bug that always throws, it’ll fail on every attempt and exhaust retries.
Your responsibility: idempotency
Section titled “Your responsibility: idempotency”Because delivery is at-least-once, your handler may run more than once for the same job. Scenarios where this happens:
- Timeout + late completion: Your handler finishes but the result report fails or arrives after the lease expired. Chronos doesn’t know it succeeded, so it retries.
- Network partition: Result reported successfully but the response is lost before the SDK receives confirmation. SDK retries the report (handled internally), but the handler itself won’t re-run in this case.
- Push: ambiguous timeout: Your endpoint processed the job but responded too slowly. Chronos timed out and retried, but your first execution already had side effects.
How to handle this:
Use job_id as an idempotency key in downstream systems. It stays the same across retries, so a payment that succeeded on a timed-out attempt won’t be charged again.
chronos.worker.handle('charge-customer', async (ctx) => { await stripe.charges.create({ amount: ctx.payload.amount, currency: 'usd', customer: ctx.payload.customerId, idempotency_key: ctx.jobId, });});For a complete guide on building idempotent handlers, see Idempotent Handlers.
What Chronos does NOT do
Section titled “What Chronos does NOT do”| Common expectation | Reality |
|---|---|
| Run your code | Chronos triggers your code. You run it on your own infrastructure. |
| Replace your job queue | Chronos schedules and delivers. If you need complex queuing (priority, concurrency limits, dead-letter), run your own queue downstream. |
| Guarantee exactly-once execution | At-least-once delivery means your handler must be safe to run twice. |
| Store your data | Payloads are passed through. Chronos doesn’t process or retain them beyond delivery. |
| Enforce timeouts | The timeout field is used for lease management and retry decisions. The SDK does not kill your handler. It’s informational. |