## 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

```text
workerd error: Durable Object alarm scheduled in the past
```

## Use this when

- `setAlarm` fails 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

```js
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

```js
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
