workerd error: Durable Object alarm scheduled in the past
Fixes Durable Object setAlarm calls failing because the timestamp is in the past. Use when alarm scheduling throws on past timestamps after clock skew, retries, or recomputation. Not for alarms that never fire, alarm handler crashes, or storage errors.
TL;DR
Durable Object alarms must be scheduled for a future time. Retried requests, recomputed delays that underflow, and clock assumptions produce past timestamps, and setAlarm rejects them. Clamp the timestamp to now-plus-a-small-delay before scheduling, and treat 'alarm in the past' as 'run now' in your logic.
The query
workerd error: Durable Object alarm scheduled in the pastUse this when
setAlarmfails naming a past timestamp.- The failure follows retries, restarts, or long-running requests.
- Alarms are computed as now plus delay and the delay sometimes goes negative.
Not for
- Alarms that schedule fine but never fire (handler or persistence problem).
- Exceptions inside the alarm handler (separate fix).
- setAlarm called outside a Durable Object (API misuse).
Steps
Step 1: Log the computed timestamp vs now
const at = Date.now() + delayMs;
console.log("scheduling alarm", JSON.stringify({ at: at, now: Date.now(), delayMs: delayMs }));Expected result: the log shows at slightly behind now. The delay went negative or the request ran long between computing and scheduling.
Step 2: Clamp to a minimum future time
const at = Math.max(Date.now() + delayMs, Date.now() + 1000);
await this.storage.setAlarm(at);Expected result: the alarm always schedules. A past-due alarm becomes 'run in one second', which matches the intent of 'this should have run already'.
Step 3: Fix the delay computation
Find where delayMs goes negative: a deadline already passed, a retry reusing a stale deadline, or arithmetic on two timestamps from different clocks.
Expected result: delays are non-negative by construction. The clamp is the safety net, not the primary fix.
Step 4: Handle the alarm handler idempotently
A clamped 'run now' alarm can fire for work that partially completed before the retry. Make the handler check whether the work is already done.
Expected result: duplicate or early alarm firings are harmless.
Step 5: Verify across restarts
Expected result: alarms scheduled before a DO eviction still fire correctly after, and recomputed alarms on wake never go backwards.
Why it happens
'Now' moves while the request runs. Code that captures a deadline early, does slow work (fetches, storage), then schedules now-plus-remaining can land in the past. Retries amplify it: the second attempt recomputes from an already-passed deadline. The API rejects past alarms instead of firing immediately, so the failure is loud.
Edge cases
- Only one alarm can be scheduled per Durable Object at a time; scheduling replaces the previous one. Clamping does not stack alarms.
- Alarm handlers run with limited wall-clock time. 'Run now' work must fit or be chunked across further alarms.
- Millisecond precision is fine, but sub-second alarms plus cold starts can still land late; design handlers to tolerate lateness.
- Do not compute the alarm time far in advance of the setAlarm call. Compute it right before scheduling.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_tgjavuuGsfGonhcEh4JGAw