ADR 0037 — Deterministic Jitter and the scheduled_for / fire_after Split
Status: Accepted Context date: 2026-08-06 Builds on: ADR 0033 (claim-queue scheduling), ADR 0034 (idempotency key)
Context
Cron traffic is pathologically synchronized: a large fraction of all schedules land on :00. Without spreading, every top of the hour is a thundering herd against the planner, the executor pool, and the network egress path. But naive random jitter breaks the tenant's exactness model — the idempotency key, the signed payload, and the run history must all agree on when the occurrence was, and replays/debugging must be reproducible.
Decision
1. Two timestamps per run, with distinct meanings
scheduled_for— the canonical, un-jittered planned instant. It is the idempotency anchor (sha256(schedule_id || scheduled_for), ADR 0034), the value in the signature payload andX-Scheduled-Forheader, the tenant-visible history timestamp, and the partition key (docs/03). Jitter never touches it.fire_after—scheduled_for + deterministic_jitter; the earliest instant the executor may claim the run. Jitter only movesfire_after.
2. Jitter is deterministic, not random
jitter = hash(schedule_id) mod jitter_seconds. The same occurrence of the same schedule always fires at the same offset — replay, reconciliation, and debugging stay predictable, and the offset is stable across replicas and retries.
3. Defaults that target the herd, not everyone
Default jitter_seconds = min(60, interval/10) for schedules landing on :00; 0 otherwise. Tenants may set 0 explicitly (exact firing) or up to 900. Jitter is one of five herd controls — alongside the planner's 5 s lookahead, the durable queue (a spike becomes backlog, not loss), backlog-driven autoscaling, and a global outbound token bucket — with the SLO: p99 fire delay ≤ 2 s normally, ≤ 15 s at :00.
Alternatives Considered
- Random per-fire jitter. Rejected: two replicas (or a retry after crash) would compute different fire instants for the same occurrence; reconciliation ("why did this fire at 12:00:37?") becomes unanswerable, and test fixtures become flaky by construction.
- No jitter, absorb the herd with capacity. Rejected: provisioning executor and egress capacity for the
:00spike wastes it the other 59 minutes; deterministic spreading is free and removes the spike at its source. The durable queue still absorbs what jitter does not. - Jittering
scheduled_foritself. Rejected outright: it would change the idempotency key per computed offset, break the tenant's "my 03:00 job" mental model in history and signatures, and couple correctness (dedup) to a load-shedding concern. - Mandatory jitter for all schedules. Rejected: some tenants legitimately need exact firing (market-open jobs); jitter defaults on only where the herd forms (
:00alignments) and stays tenant-overridable to0.
Consequences
- Positive: Herd control is invisible to the tenant's correctness model — keys, signatures, and history all speak
scheduled_for; only wall-clock delivery time moves, within a bounded, documented window. - Positive: Determinism makes the fire time of any occurrence computable from the schedule row alone — support can answer "when will/did it fire" without logs.
- Negative / accepted: A
:00schedule with default jitter fires up to 60 s "late" by wall clock. Documented; tenants who care setjitter_seconds: 0. - Negative / accepted: Deterministic offsets mean one schedule's offset never changes — a tenant whose jittered slot collides with their own other traffic must adjust
jitter_secondsmanually rather than benefiting from re-rolls. - Interaction:
fire_afteris what the executor claim index orders on (docs/03job_run_claim_idxvianext_attempt_at); the SLO metricscheduler_fire_delay_secondsmeasuresscheduled_for→ first attempt, so jitter is visible in the histogram by design.