Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,12 @@ class PollingAgent extends Agent {

</TypeScriptExample>

:::caution[Avoid scheduling loops]
A callback that reschedules itself with a delay of `0`, or with a `Date` that has already passed, runs again almost immediately. If nothing changes between runs, the callback loops continuously. Scheduled tasks run on [Durable Object alarms](/durable-objects/api/alarms/), so a loop adds billed requests and storage operations until it stops.

Only reschedule when there is more work to do, and use a non-zero delay. For work that repeats on a fixed cadence, use [`scheduleEvery()`](#interval) instead of rescheduling from the callback.
:::

### Exponential backoff retry

<TypeScriptExample>
Expand Down
1 change: 1 addition & 0 deletions src/content/docs/billing/manage/optimize-costs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Cached responses are the cheapest path through Cloudflare. Every cache hit avoid
|---|---|
| Use [Workers Logs](/workers/observability/logs/workers-logs/) for log retention instead of Logpush | Logpush-enabled Workers requests and Logpush data transfer |
| Use the [WebSocket Hibernation API](/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api) for Durable Objects with idle WebSocket connections | Duration (GB-s) charges — billing pauses while the Durable Object is hibernated |
| Use a minimum delay and a deadline when you [reschedule Durable Object alarms](/durable-objects/best-practices/rules-of-durable-objects/#do-not-reschedule-alarms-without-a-delay-and-a-stop-condition) | Requests and rows written by alarm loops |

### R2

Expand Down
11 changes: 11 additions & 0 deletions src/content/docs/durable-objects/api/alarms.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ Notably:
- Alarms have guaranteed at-least-once execution and are retried automatically when the `alarm()` handler throws.
- Retries are performed using exponential backoff starting at a 2 second delay from the first failure with up to 6 retries allowed.

:::caution[Avoid alarm loops]
If your `alarm()` handler calls `setAlarm()` with the current time or a time in the past, the handler is rescheduled to run immediately. If nothing changes between runs, the handler keeps rescheduling itself. A single Durable Object in this loop can run its alarm hundreds of times per second until the code changes or the alarm is deleted.

Each alarm invocation is billed as a request. Each `setAlarm()` call is billed as a row written. When you reschedule from inside `alarm()`, enforce a minimum delay, back off for retried operations, and stop at a deadline.

For more information, refer to [Do not reschedule alarms without a delay and a stop condition](/durable-objects/best-practices/rules-of-durable-objects/#do-not-reschedule-alarms-without-a-delay-and-a-stop-condition).
:::

:::note[How are alarms different from Cron Triggers?]

Alarms are more fine grained than [Cron Triggers](/workers/configuration/cron-triggers/). A Worker can have up to three Cron Triggers configured at once, but it can have an unlimited amount of Durable Objects, each of which can have an alarm set.
Expand Down Expand Up @@ -93,6 +101,7 @@ export class AgentServer extends DurableObject {

- Set the time for the alarm to run. Specify the time as the number of milliseconds elapsed since the UNIX epoch.
- If you call `setAlarm` when there is already one scheduled, it will override the existing alarm.
- If you call `setAlarm` with the current time or a time in the past, the alarm is scheduled to run immediately. Calling it this way from inside `alarm()` can create an alarm loop. For more information, refer to [Do not reschedule alarms without a delay and a stop condition](/durable-objects/best-practices/rules-of-durable-objects/#do-not-reschedule-alarms-without-a-delay-and-a-stop-condition).

:::caution[Calling `setAlarm` inside the constructor]
If you wish to call `setAlarm` inside the constructor of a Durable Object, ensure that you are first checking whether an alarm has already been set.
Expand Down Expand Up @@ -130,6 +139,8 @@ This is due to the fact that, if the Durable Object wakes up after being inactiv

Because alarms are only retried up to 6 times on error, it's recommended to catch any exceptions inside your `alarm()` handler and schedule a new alarm before returning if you want to make sure your alarm handler will be retried indefinitely. Otherwise, a sufficiently long outage in a downstream service that you depend on or a bug in your code that goes unfixed for hours can exhaust the limited number of retries, causing the alarm to not be re-run in the future until the next time you call `setAlarm`.

When you schedule your own retries, schedule them in the future. Increase the delay after each failure. Stop at a deadline, so that a permanent failure does not reschedule the alarm forever. Base the deadline on state the object already stores, such as the time the work became due. Unlike an in-memory counter, this survives eviction and needs no extra storage writes. `alarmInfo.retryCount` does not count alarms you schedule yourself.

:::

## Example
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,7 @@ Key points about alarms:
- **`setAlarm(timestamp)`** schedules the `alarm()` handler to run at any time in the future (millisecond precision)
- **Alarms do not repeat automatically** — you must call `setAlarm()` again to schedule the next execution
- **Only schedule alarms when there is work to do** — avoid waking up every Durable Object on short intervals (seconds), as each alarm invocation incurs costs
- **Reschedule with a delay and a stop condition** — rescheduling to the current time from `alarm()` creates an [alarm loop](#do-not-reschedule-alarms-without-a-delay-and-a-stop-condition)

<TypeScriptExample filename="index.ts">
```ts
Expand Down Expand Up @@ -1483,6 +1484,139 @@ export default {

This pattern does not scale. As traffic increases, the single Durable Object becomes a chokepoint. Instead, identify natural coordination boundaries in your application (per user, per room, per document) and create separate Durable Objects for each.

### Do not reschedule alarms without a delay and a stop condition

If `alarm()` calls `setAlarm()` with the current time or a time in the past, the alarm is rescheduled to run immediately. If the condition that caused the reschedule does not change, the handler reschedules itself again. This creates an alarm loop.

A single Durable Object in an alarm loop can run its alarm hundreds of times per second. The alarm is stored with the object, so the loop continues across evictions and deployments. It stops only when the handler stops rescheduling or the alarm is deleted.

Alarm loops are expensive even though each invocation does little work:

- Each alarm invocation is billed as a request.
- Each `setAlarm()` call is billed as a row written.
- Each read and write the handler performs is billed as rows read and rows written.

Compute duration stays relatively low, because a looping object is billed as one continuously active instance. Most of the cost appears as requests and rows written. For rates, refer to [Durable Objects pricing](/durable-objects/platform/pricing/).

To prevent alarm loops:

- **Enforce a minimum delay when you reschedule from `alarm()`.** Route every reschedule through one helper that clamps the time to at least `Date.now()` plus a minimum delay.
- **Back off for retried operations.** Increase the delay after each failed attempt instead of retrying at a fixed short interval.
- **Stop at a deadline.** Base it on state the object already stores, such as when the work became due. Unlike an in-memory counter, this survives eviction and needs no extra storage writes. `alarmInfo.retryCount` does not count alarms you schedule yourself.
- **Return without rescheduling when there is no work.** An alarm handler does not need to call `setAlarm()` on every run.
- **Test the next alarm time.** Refer to [Test that alarms reschedule safely](/durable-objects/examples/testing-with-durable-objects/#test-that-alarms-reschedule-safely).
- **Set a [budget alert](/billing/manage/budget-alerts/).** Budget alerts notify you when usage-based spend crosses a dollar threshold.

<TypeScriptExample filename="index.ts">
```ts
import { DurableObject } from "cloudflare:workers";

export interface Env {
SESSION: DurableObjectNamespace<Session>;
}

interface Checkpoint {
refreshAt: number;
}

// 🔴 Bad: Reschedules to "now" while the refresh window stays open
export class Session extends DurableObject<Env> {
async alarm() {
const checkpoint = await this.ctx.storage.get<Checkpoint>("checkpoint");
if (!checkpoint || Date.now() < checkpoint.refreshAt) {
return;
}

const refreshed = await this.tryRefresh();
if (!refreshed) {
// The alarm runs again immediately. If tryRefresh() keeps failing,
// this loops until the code changes or the alarm is deleted.
// setAlarm(checkpoint.refreshAt) has the same effect, because
// refreshAt is already in the past.
await this.ctx.storage.setAlarm(Date.now());
}
}

private async tryRefresh(): Promise<boolean> {
const response = await fetch("https://api.example.com/refresh", {
method: "POST",
});
return response.ok;
}
}
```
</TypeScriptExample>

Instead, enforce a minimum delay, back off for retried operations, and stop at a deadline. The following example derives both the backoff and the deadline from `refreshAt`, so retries need no extra storage writes:

<TypeScriptExample filename="index.ts">
```ts
import { DurableObject } from "cloudflare:workers";

export interface Env {
SESSION: DurableObjectNamespace<Session>;
}

interface Checkpoint {
refreshAt: number;
}

const MIN_DELAY_MS = 60 * 1000; // 1 minute
const MAX_DELAY_MS = 60 * 60 * 1000; // 1 hour
const GIVE_UP_AFTER_MS = 24 * 60 * 60 * 1000; // 1 day
const REFRESH_INTERVAL_MS = 60 * 60 * 1000; // 1 hour

// ✅ Good: Minimum delay, backoff, and a deadline, with no extra storage writes
export class Session extends DurableObject<Env> {
async alarm() {
const checkpoint = await this.ctx.storage.get<Checkpoint>("checkpoint");
if (!checkpoint) {
return; // No work to do, so do not reschedule
}

const overdueMs = Date.now() - checkpoint.refreshAt;
if (overdueMs < 0) {
await this.rescheduleAlarm(checkpoint.refreshAt);
return;
}

if (await this.tryRefresh()) {
// Move the deadline forward by scheduling the next refresh
const next: Checkpoint = { refreshAt: Date.now() + REFRESH_INTERVAL_MS };
await this.ctx.storage.put("checkpoint", next);
await this.rescheduleAlarm(next.refreshAt);
return;
}

if (overdueMs >= GIVE_UP_AFTER_MS) {
// Stop rescheduling and surface the failure instead
console.error(`Checkpoint refresh failed for ${overdueMs} ms, giving up`);
return;
}

// Back off: wait one to two times as long as the refresh has been failing.
// The random factor spreads out retries from objects that fail at the same time.
const backoff = overdueMs * (1.0 + Math.random());
await this.rescheduleAlarm(Date.now() + Math.min(backoff, MAX_DELAY_MS));
}

// Every reschedule from alarm() goes through this helper
private async rescheduleAlarm(runAt: number) {
await this.ctx.storage.setAlarm(Math.max(runAt, Date.now() + MIN_DELAY_MS));
}

private async tryRefresh(): Promise<boolean> {
const response = await fetch("https://api.example.com/refresh", {
method: "POST",
});
return response.ok;
}
}
```
</TypeScriptExample>

To stop an alarm loop that is already running, refer to [Unexpected charges from an alarm loop](/durable-objects/observability/troubleshooting/#unexpected-charges-from-an-alarm-loop).

## Testing and class lifecycle

### Test with Vitest and plan for class lifecycle changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,65 @@ export class Counter extends DurableObject {
```
</TypeScriptExample>

### Test that alarms reschedule safely

An `alarm()` handler that reschedules itself to the current time creates an [alarm loop](/durable-objects/best-practices/rules-of-durable-objects/#do-not-reschedule-alarms-without-a-delay-and-a-stop-condition). Alarm loops are easy to miss in review and expensive in production. Add regression tests for both guardrails. One test checks that a failed attempt reschedules the alarm at least a minimum delay in the future. Another test checks that the alarm stops rescheduling after the deadline.

The following tests use the `Session` class from [Do not reschedule alarms without a delay and a stop condition](/durable-objects/best-practices/rules-of-durable-objects/#do-not-reschedule-alarms-without-a-delay-and-a-stop-condition). The tests mock `fetch()` so that every refresh fails. Each test stores a checkpoint that became due at a chosen time in the past, runs the alarm once, and checks the next scheduled alarm:

<TypeScriptExample filename="test/alarm-loop.test.ts">
```ts
import { env } from "cloudflare:workers";
import { runInDurableObject, runDurableObjectAlarm } from "cloudflare:test";
import { afterEach, beforeEach, describe, it, expect, vi } from "vitest";

const MIN_DELAY_MS = 60 * 1000;
const GIVE_UP_AFTER_MS = 24 * 60 * 60 * 1000;

beforeEach(() => {
// Force every refresh attempt to fail
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(null, { status: 503 }),
);
});

afterEach(() => {
vi.restoreAllMocks();
});

// Store a checkpoint that became due `overdueMs` ago, run its alarm once,
// and return the next scheduled alarm time
async function runFailedRefresh(name: string, overdueMs: number) {
const stub = env.SESSION.getByName(name);

await runInDurableObject(stub, async (_instance, state) => {
await state.storage.put("checkpoint", { refreshAt: Date.now() - overdueMs });
// Schedule in the future so the alarm only runs when the test triggers it
await state.storage.setAlarm(Date.now() + MIN_DELAY_MS);
});

expect(await runDurableObjectAlarm(stub)).toBe(true);
return runInDurableObject(stub, (_instance, state) => state.storage.getAlarm());
}

describe("Session alarm", () => {
it("reschedules a failed refresh at least the minimum delay in the future", async () => {
const nextAlarm = await runFailedRefresh("retry", 1000);
expect(nextAlarm).not.toBeNull();
// Allow for time that passes while the test runs
expect(nextAlarm).toBeGreaterThan(Date.now() + MIN_DELAY_MS / 2);
});

it("stops rescheduling after the deadline", async () => {
const nextAlarm = await runFailedRefresh("give-up", GIVE_UP_AFTER_MS + 1000);
expect(nextAlarm).toBeNull();
});
});
```
</TypeScriptExample>

If a future change reintroduces `setAlarm(Date.now())`, the first test fails. If a change removes the deadline, the second test fails.

### Testing eviction

Use `evictDurableObject()` to evict a Durable Object instance during tests. Eviction tears down the instance to reset its in-memory state. This lets you test how your Durable Object recovers state from storage after being evicted.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,32 @@ products:

`wrangler tail` displays a live feed of console and exception logs for each request served by your Worker code, including both normal Worker requests and Durable Object requests. After running `npx wrangler deploy`, you can use `wrangler tail` in the root directory of your Worker project and visit your Worker URL to see console and error logs in your terminal.

## Unexpected charges from an alarm loop

An alarm loop happens when an `alarm()` handler reschedules itself to the current time or a time in the past. The alarm then runs continuously. For background and prevention, refer to [Do not reschedule alarms without a delay and a stop condition](/durable-objects/best-practices/rules-of-durable-objects/#do-not-reschedule-alarms-without-a-delay-and-a-stop-condition).

### Symptoms

- The [billable usage dashboard](/billing/manage/billable-usage/) shows a sudden increase in Durable Objects requests and rows written.
- The namespace **Metrics** tab shows a high, flat request rate that does not follow your traffic. Refer to [View metrics and analytics](/durable-objects/observability/metrics-and-analytics/#view-metrics-and-analytics).
- [Workers Logs](/workers/observability/logs/workers-logs/) or `wrangler tail` show the same Durable Object running `alarm` invocations back to back.

To find the affected object, filter the **Metrics** tab by Durable Object [ID](/durable-objects/api/id/) or [name](/durable-objects/api/id/#name).

### Stop the loop

The alarm is stored with the Durable Object, so the loop continues until the handler stops rescheduling or the alarm is deleted. Use one of the following options:

- **Deploy a fix.** The next alarm run uses your new code. Make the handler return without calling `setAlarm()`, or enforce a minimum delay.
- **Delete the alarm.** Call [`deleteAlarm()`](/durable-objects/api/alarms/#deletealarm) on the affected object, for example from an RPC method you call once.
- **Delete all storage for the object.** If you no longer need the object's data, call [`deleteAll()`](/durable-objects/api/sqlite-storage-api/#deleteall). With a compatibility date of `2026-02-24` or later, `deleteAll()` also deletes the alarm.

### Prevent alarm loops

To catch an alarm loop before you deploy, add a regression test. For more information, refer to [Test that alarms reschedule safely](/durable-objects/examples/testing-with-durable-objects/#test-that-alarms-reschedule-safely).

To find out about unexpected spend sooner, set up [budget alerts](/billing/manage/budget-alerts/). Usage is processed once per day, so a budget alert fires the day after spend crosses your threshold.

## Common errors

### No event handlers were registered. This script does nothing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Type, MetaInfo, Render } from "~/components";
- <code>setAlarm(scheduledTime <Type text="Date | number" />, options <Type text="Object" /> <MetaInfo text="optional" />)</code>: <Type text="Promise" />
- Sets the current alarm time, accepting either a JavaScript `Date`, or integer milliseconds since epoch.

If `setAlarm()` is called with a time equal to or before `Date.now()`, the alarm will be scheduled for asynchronous execution in the immediate future. If the alarm handler is currently executing in this case, it will not be canceled. Alarms can be set to millisecond granularity and will usually execute within a few milliseconds after the set time, but can be delayed by up to a minute due to maintenance or failures while failover takes place.
If `setAlarm()` is called with a time equal to or before `Date.now()`, the alarm will be scheduled for asynchronous execution in the immediate future. If the alarm handler is currently executing in this case, it will not be canceled. If you call `setAlarm()` this way from inside `alarm()`, the handler is rescheduled to run immediately and can loop. For more information, refer to [Do not reschedule alarms without a delay and a stop condition](/durable-objects/best-practices/rules-of-durable-objects/#do-not-reschedule-alarms-without-a-delay-and-a-stop-condition). Alarms can be set to millisecond granularity and will usually execute within a few milliseconds after the set time, but can be delayed by up to a minute due to maintenance or failures while failover takes place.

### `deleteAlarm`

Expand Down
Loading