From c070a46fd263267cf8e4d95c5d798072609a76d9 Mon Sep 17 00:00:00 2001 From: Harshil Agrawal Date: Fri, 9 Oct 2026 13:52:11 +0200 Subject: [PATCH 1/5] update docs to tighten up alarm loop guardrails --- .../runtime/execution/schedule-tasks.mdx | 6 + .../docs/billing/manage/optimize-costs.mdx | 1 + .../docs/durable-objects/api/alarms.mdx | 11 ++ .../rules-of-durable-objects.mdx | 132 ++++++++++++++++++ .../examples/testing-with-durable-objects.mdx | 61 ++++++++ .../observability/troubleshooting.mdx | 26 ++++ .../durable-objects/api-storage-alarms.mdx | 2 +- 7 files changed, 238 insertions(+), 1 deletion(-) diff --git a/src/content/docs/agents/runtime/execution/schedule-tasks.mdx b/src/content/docs/agents/runtime/execution/schedule-tasks.mdx index 94bb7158614..c5488f0ac98 100644 --- a/src/content/docs/agents/runtime/execution/schedule-tasks.mdx +++ b/src/content/docs/agents/runtime/execution/schedule-tasks.mdx @@ -490,6 +490,12 @@ class PollingAgent extends Agent { +:::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 writes until it stops. + +When you compute a delay at runtime, enforce a minimum value. Stop after a fixed number of failed attempts, as shown in [Exponential backoff retry](#exponential-backoff-retry). For fixed intervals, use [`scheduleEvery()`](#interval). 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). +::: + ### Exponential backoff retry diff --git a/src/content/docs/billing/manage/optimize-costs.mdx b/src/content/docs/billing/manage/optimize-costs.mdx index 1ebb06ae5ce..a44f02ffed6 100644 --- a/src/content/docs/billing/manage/optimize-costs.mdx +++ b/src/content/docs/billing/manage/optimize-costs.mdx @@ -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 an attempt limit 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 diff --git a/src/content/docs/durable-objects/api/alarms.mdx b/src/content/docs/durable-objects/api/alarms.mdx index 4d576fbe8cd..08bc41f9837 100644 --- a/src/content/docs/durable-objects/api/alarms.mdx +++ b/src/content/docs/durable-objects/api/alarms.mdx @@ -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 runs again almost 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 after failures, and stop after a fixed number of attempts. + +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. @@ -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 runs almost 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. @@ -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. Store an attempt count in storage and stop after a limit, so that a permanent failure does not reschedule the alarm forever. `alarmInfo.retryCount` does not count alarms you schedule yourself. + ::: ## Example diff --git a/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx b/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx index 2ad94cdb633..b946ba849e6 100644 --- a/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx +++ b/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx @@ -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) ```ts @@ -1483,6 +1484,137 @@ 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 runs again almost 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 on failure.** Increase the delay after each failed attempt instead of retrying at a fixed short interval. +- **Store an attempt counter and stop at a limit.** Keep the counter in storage so it survives eviction. `alarmInfo.retryCount` only counts automatic retries of the current alarm after the handler throws. It 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. + + +```ts +import { DurableObject } from "cloudflare:workers"; + +export interface Env { + SESSION: DurableObjectNamespace; +} + +interface Checkpoint { + refreshAt: number; +} + +// 🔴 Bad: Reschedules to "now" while the refresh window stays open +export class Session extends DurableObject { + async alarm() { + const checkpoint = await this.ctx.storage.get("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 { + const response = await fetch("https://api.example.com/refresh", { + method: "POST", + }); + return response.ok; + } +} +``` + + +Instead, enforce a minimum delay, back off on failure, and stop after a fixed number of attempts: + + +```ts +import { DurableObject } from "cloudflare:workers"; + +export interface Env { + SESSION: DurableObjectNamespace; +} + +interface Checkpoint { + refreshAt: number; +} + +const MIN_DELAY_MS = 60 * 1000; // 1 minute +const MAX_DELAY_MS = 60 * 60 * 1000; // 1 hour +const MAX_ATTEMPTS = 8; + +// ✅ Good: Minimum delay, exponential backoff, and a hard stop +export class Session extends DurableObject { + async alarm() { + const checkpoint = await this.ctx.storage.get("checkpoint"); + if (!checkpoint) { + return; // No work to do, so do not reschedule + } + + if (Date.now() < checkpoint.refreshAt) { + await this.rescheduleAlarm(checkpoint.refreshAt); + return; + } + + if (await this.tryRefresh()) { + await this.ctx.storage.delete("refreshAttempts"); + return; + } + + const attempts = + ((await this.ctx.storage.get("refreshAttempts")) ?? 0) + 1; + + if (attempts >= MAX_ATTEMPTS) { + // Stop rescheduling and surface the failure instead + console.error(`Checkpoint refresh failed ${attempts} times, giving up`); + await this.ctx.storage.delete("refreshAttempts"); + return; + } + + await this.ctx.storage.put("refreshAttempts", attempts); + const backoff = Math.min(MIN_DELAY_MS * 2 ** attempts, MAX_DELAY_MS); + await this.rescheduleAlarm(Date.now() + backoff); + } + + // 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 { + const response = await fetch("https://api.example.com/refresh", { + method: "POST", + }); + return response.ok; + } +} +``` + + +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 diff --git a/src/content/docs/durable-objects/examples/testing-with-durable-objects.mdx b/src/content/docs/durable-objects/examples/testing-with-durable-objects.mdx index 3d76210f3af..d41ac867835 100644 --- a/src/content/docs/durable-objects/examples/testing-with-durable-objects.mdx +++ b/src/content/docs/durable-objects/examples/testing-with-durable-objects.mdx @@ -458,6 +458,67 @@ export class Counter extends DurableObject { ``` +### 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 a regression test that runs the alarm repeatedly. The test checks that every rescheduled alarm is at least a minimum delay in the future. It also checks that the alarm stops rescheduling after a fixed number of failed attempts. + +The following test uses 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). It mocks `fetch()` so that every refresh fails, and records the delay of each `setAlarm()` call. It then runs the alarm until no alarm is scheduled: + + +```ts +import { env } from "cloudflare:workers"; +import { runInDurableObject, runDurableObjectAlarm } from "cloudflare:test"; +import { afterEach, describe, it, expect, vi } from "vitest"; + +const MIN_DELAY_MS = 60 * 1000; +const MAX_ATTEMPTS = 8; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("Session alarm", () => { + it("reschedules failed refreshes in the future and stops at the limit", async () => { + // Force every refresh attempt to fail + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response(null, { status: 503 }), + ); + + const stub = env.SESSION.getByName("alarm-loop-test"); + const delays: number[] = []; + + await runInDurableObject(stub, async (_instance, state) => { + await state.storage.put("checkpoint", { refreshAt: Date.now() - 1000 }); + // Schedule in the future so the alarm only runs when the test triggers it + await state.storage.setAlarm(Date.now() + MIN_DELAY_MS); + + // Record how far in the future the handler schedules each alarm + const setAlarm = state.storage.setAlarm.bind(state.storage); + vi.spyOn(state.storage, "setAlarm").mockImplementation((time, options) => { + delays.push(Number(time) - Date.now()); + return setAlarm(time, options); + }); + }); + + let runs = 0; + while (await runDurableObjectAlarm(stub)) { + runs++; + // Fails the test instead of hanging if the alarm never stops + expect(runs).toBeLessThanOrEqual(MAX_ATTEMPTS); + } + + // A rescheduled alarm must never be due immediately + for (const delay of delays) { + expect(delay).toBeGreaterThanOrEqual(MIN_DELAY_MS); + } + expect(runs).toBe(MAX_ATTEMPTS); + }); +}); +``` + + +If a future change reintroduces `setAlarm(Date.now())`, the minimum delay assertion fails. If a change removes the attempt limit, the run count assertion 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. diff --git a/src/content/docs/durable-objects/observability/troubleshooting.mdx b/src/content/docs/durable-objects/observability/troubleshooting.mdx index 9deac75a6b4..40874058bd3 100644 --- a/src/content/docs/durable-objects/observability/troubleshooting.mdx +++ b/src/content/docs/durable-objects/observability/troubleshooting.mdx @@ -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. diff --git a/src/content/partials/durable-objects/api-storage-alarms.mdx b/src/content/partials/durable-objects/api-storage-alarms.mdx index 8deccef0d3d..d69415efc4d 100644 --- a/src/content/partials/durable-objects/api-storage-alarms.mdx +++ b/src/content/partials/durable-objects/api-storage-alarms.mdx @@ -18,7 +18,7 @@ import { Type, MetaInfo, Render } from "~/components"; - setAlarm(scheduledTime , options ): - 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 runs again almost 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` From cb88a4027c618e0733b6b8251ffbab63b5328ca9 Mon Sep 17 00:00:00 2001 From: Harshil Agrawal <18901032+harshil1712@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:16:05 +0200 Subject: [PATCH 2/5] Apply batched suggestions from code review Co-authored-by: Ashley Peacock --- src/content/docs/durable-objects/api/alarms.mdx | 4 ++-- .../best-practices/rules-of-durable-objects.mdx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/content/docs/durable-objects/api/alarms.mdx b/src/content/docs/durable-objects/api/alarms.mdx index 08bc41f9837..d5c435d21de 100644 --- a/src/content/docs/durable-objects/api/alarms.mdx +++ b/src/content/docs/durable-objects/api/alarms.mdx @@ -21,7 +21,7 @@ Notably: - 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 runs again almost 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. +If your `alarm()` handler calls `setAlarm()` with the current time or a time in the past, the handler runs again 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 after failures, and stop after a fixed number of attempts. @@ -101,7 +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 runs almost 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). + - If you call `setAlarm` with the current time or a time in the past, the alarm runs 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. diff --git a/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx b/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx index b946ba849e6..46bc4031fcc 100644 --- a/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx +++ b/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx @@ -1486,7 +1486,7 @@ This pattern does not scale. As traffic increases, the single Durable Object bec ### 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 runs again almost immediately. If the condition that caused the reschedule does not change, the handler reschedules itself again. This creates an alarm loop. +If `alarm()` calls `setAlarm()` with the current time or a time in the past, the alarm runs again 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. From 0af9e6a7826b75aca6f0d8fcb9c28c9a14ad7d3c Mon Sep 17 00:00:00 2001 From: Harshil Agrawal Date: Mon, 12 Oct 2026 14:42:54 +0200 Subject: [PATCH 3/5] Use a deadline instead of a stored attempt counter for alarm retries Addresses review feedback that storing an attempt counter adds a write per alarm. An in-memory counter is not a reliable substitute: objects are evicted after 70-140s idle, so with retries >= 1 minute apart it resets between attempts and the limit never trips. - Rules of Durable Objects: the good example derives both backoff and a give-up deadline from `refreshAt`, which the object already stores. Retries need no extra storage writes, and the stop condition survives eviction. The success path now advances `refreshAt` so the deadline stays meaningful. - Alarms API: update the caution and the "Catching exceptions" note to match. - Testing: replace the run-until-stop test with two focused tests (minimum delay on retry, no reschedule after the deadline). - Storage API partial: "almost immediately" -> "immediately", consistent with earlier review suggestions. - Optimize costs: "attempt limit" -> "deadline". Verified: examples and tests type-check (strict, noUnused*); good example passes; setAlarm(Date.now()) variant fails test 1; no-deadline variant fails test 2. pnpm run check and pnpm run build pass. --- .../docs/billing/manage/optimize-costs.mdx | 2 +- .../docs/durable-objects/api/alarms.mdx | 4 +- .../rules-of-durable-objects.mdx | 30 ++++---- .../examples/testing-with-durable-objects.mdx | 70 +++++++++---------- .../durable-objects/api-storage-alarms.mdx | 2 +- 5 files changed, 53 insertions(+), 55 deletions(-) diff --git a/src/content/docs/billing/manage/optimize-costs.mdx b/src/content/docs/billing/manage/optimize-costs.mdx index a44f02ffed6..c2657338d48 100644 --- a/src/content/docs/billing/manage/optimize-costs.mdx +++ b/src/content/docs/billing/manage/optimize-costs.mdx @@ -35,7 +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 an attempt limit 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 | +| 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 diff --git a/src/content/docs/durable-objects/api/alarms.mdx b/src/content/docs/durable-objects/api/alarms.mdx index d5c435d21de..e13a4732140 100644 --- a/src/content/docs/durable-objects/api/alarms.mdx +++ b/src/content/docs/durable-objects/api/alarms.mdx @@ -23,7 +23,7 @@ Notably: :::caution[Avoid alarm loops] If your `alarm()` handler calls `setAlarm()` with the current time or a time in the past, the handler runs again 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 after failures, and stop after a fixed number of attempts. +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 after failures, 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). ::: @@ -139,7 +139,7 @@ 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. Store an attempt count in storage and stop after a limit, so that a permanent failure does not reschedule the alarm forever. `alarmInfo.retryCount` does not count alarms you schedule yourself. +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. ::: diff --git a/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx b/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx index 46bc4031fcc..c9f253c464a 100644 --- a/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx +++ b/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx @@ -1502,7 +1502,7 @@ 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 on failure.** Increase the delay after each failed attempt instead of retrying at a fixed short interval. -- **Store an attempt counter and stop at a limit.** Keep the counter in storage so it survives eviction. `alarmInfo.retryCount` only counts automatic retries of the current alarm after the handler throws. It does not count alarms you schedule yourself. +- **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. @@ -1547,7 +1547,7 @@ export class Session extends DurableObject { ``` -Instead, enforce a minimum delay, back off on failure, and stop after a fixed number of attempts: +Instead, enforce a minimum delay, back off on failure, and stop at a deadline. The following example derives both the backoff and the deadline from `refreshAt`, so retries need no extra storage writes: ```ts @@ -1563,9 +1563,10 @@ interface Checkpoint { const MIN_DELAY_MS = 60 * 1000; // 1 minute const MAX_DELAY_MS = 60 * 60 * 1000; // 1 hour -const MAX_ATTEMPTS = 8; +const GIVE_UP_AFTER_MS = 24 * 60 * 60 * 1000; // 1 day +const REFRESH_INTERVAL_MS = 60 * 60 * 1000; // 1 hour -// ✅ Good: Minimum delay, exponential backoff, and a hard stop +// ✅ Good: Minimum delay, backoff, and a deadline, with no extra storage writes export class Session extends DurableObject { async alarm() { const checkpoint = await this.ctx.storage.get("checkpoint"); @@ -1573,29 +1574,28 @@ export class Session extends DurableObject { return; // No work to do, so do not reschedule } - if (Date.now() < checkpoint.refreshAt) { + const overdueMs = Date.now() - checkpoint.refreshAt; + if (overdueMs < 0) { await this.rescheduleAlarm(checkpoint.refreshAt); return; } if (await this.tryRefresh()) { - await this.ctx.storage.delete("refreshAttempts"); + // 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; } - const attempts = - ((await this.ctx.storage.get("refreshAttempts")) ?? 0) + 1; - - if (attempts >= MAX_ATTEMPTS) { + if (overdueMs >= GIVE_UP_AFTER_MS) { // Stop rescheduling and surface the failure instead - console.error(`Checkpoint refresh failed ${attempts} times, giving up`); - await this.ctx.storage.delete("refreshAttempts"); + console.error(`Checkpoint refresh failed for ${overdueMs} ms, giving up`); return; } - await this.ctx.storage.put("refreshAttempts", attempts); - const backoff = Math.min(MIN_DELAY_MS * 2 ** attempts, MAX_DELAY_MS); - await this.rescheduleAlarm(Date.now() + backoff); + // Back off: wait about as long as the refresh has been failing + await this.rescheduleAlarm(Date.now() + Math.min(overdueMs, MAX_DELAY_MS)); } // Every reschedule from alarm() goes through this helper diff --git a/src/content/docs/durable-objects/examples/testing-with-durable-objects.mdx b/src/content/docs/durable-objects/examples/testing-with-durable-objects.mdx index d41ac867835..5dd5f78d8b8 100644 --- a/src/content/docs/durable-objects/examples/testing-with-durable-objects.mdx +++ b/src/content/docs/durable-objects/examples/testing-with-durable-objects.mdx @@ -460,64 +460,62 @@ export class Counter extends DurableObject { ### 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 a regression test that runs the alarm repeatedly. The test checks that every rescheduled alarm is at least a minimum delay in the future. It also checks that the alarm stops rescheduling after a fixed number of failed attempts. +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 test uses 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). It mocks `fetch()` so that every refresh fails, and records the delay of each `setAlarm()` call. It then runs the alarm until no alarm is scheduled: +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: ```ts import { env } from "cloudflare:workers"; import { runInDurableObject, runDurableObjectAlarm } from "cloudflare:test"; -import { afterEach, describe, it, expect, vi } from "vitest"; +import { afterEach, beforeEach, describe, it, expect, vi } from "vitest"; const MIN_DELAY_MS = 60 * 1000; -const MAX_ATTEMPTS = 8; +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(); }); -describe("Session alarm", () => { - it("reschedules failed refreshes in the future and stops at the limit", async () => { - // Force every refresh attempt to fail - vi.spyOn(globalThis, "fetch").mockResolvedValue( - new Response(null, { status: 503 }), - ); +// 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); - const stub = env.SESSION.getByName("alarm-loop-test"); - const delays: number[] = []; - - await runInDurableObject(stub, async (_instance, state) => { - await state.storage.put("checkpoint", { refreshAt: Date.now() - 1000 }); - // Schedule in the future so the alarm only runs when the test triggers it - await state.storage.setAlarm(Date.now() + MIN_DELAY_MS); + 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); + }); - // Record how far in the future the handler schedules each alarm - const setAlarm = state.storage.setAlarm.bind(state.storage); - vi.spyOn(state.storage, "setAlarm").mockImplementation((time, options) => { - delays.push(Number(time) - Date.now()); - return setAlarm(time, options); - }); - }); + expect(await runDurableObjectAlarm(stub)).toBe(true); + return runInDurableObject(stub, (_instance, state) => state.storage.getAlarm()); +} - let runs = 0; - while (await runDurableObjectAlarm(stub)) { - runs++; - // Fails the test instead of hanging if the alarm never stops - expect(runs).toBeLessThanOrEqual(MAX_ATTEMPTS); - } +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); + }); - // A rescheduled alarm must never be due immediately - for (const delay of delays) { - expect(delay).toBeGreaterThanOrEqual(MIN_DELAY_MS); - } - expect(runs).toBe(MAX_ATTEMPTS); + it("stops rescheduling after the deadline", async () => { + const nextAlarm = await runFailedRefresh("give-up", GIVE_UP_AFTER_MS + 1000); + expect(nextAlarm).toBeNull(); }); }); ``` -If a future change reintroduces `setAlarm(Date.now())`, the minimum delay assertion fails. If a change removes the attempt limit, the run count assertion fails. +If a future change reintroduces `setAlarm(Date.now())`, the first test fails. If a change removes the deadline, the second test fails. ### Testing eviction diff --git a/src/content/partials/durable-objects/api-storage-alarms.mdx b/src/content/partials/durable-objects/api-storage-alarms.mdx index d69415efc4d..ef683dd6a91 100644 --- a/src/content/partials/durable-objects/api-storage-alarms.mdx +++ b/src/content/partials/durable-objects/api-storage-alarms.mdx @@ -18,7 +18,7 @@ import { Type, MetaInfo, Render } from "~/components"; - setAlarm(scheduledTime , options ): - 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. If you call `setAlarm()` this way from inside `alarm()`, the handler runs again almost 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. + 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 runs again 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` From 70b8bd6a304be7341fe091e562ac9bc1bfdfab84 Mon Sep 17 00:00:00 2001 From: Harshil Agrawal Date: Wed, 14 Oct 2026 11:52:55 +0200 Subject: [PATCH 4/5] Apply Agents team wording to the scheduling loop caution Use the wording suggested by the Agents team in review. The only change from the suggested text is the scheduleEvery() link anchor (#interval). --- src/content/docs/agents/runtime/execution/schedule-tasks.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/docs/agents/runtime/execution/schedule-tasks.mdx b/src/content/docs/agents/runtime/execution/schedule-tasks.mdx index c5488f0ac98..c84398db430 100644 --- a/src/content/docs/agents/runtime/execution/schedule-tasks.mdx +++ b/src/content/docs/agents/runtime/execution/schedule-tasks.mdx @@ -491,9 +491,9 @@ class PollingAgent extends Agent { :::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 writes until it stops. +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. -When you compute a delay at runtime, enforce a minimum value. Stop after a fixed number of failed attempts, as shown in [Exponential backoff retry](#exponential-backoff-retry). For fixed intervals, use [`scheduleEvery()`](#interval). 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). +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 From d41cba612d64032ad098b0d0835cc95d0e570ad3 Mon Sep 17 00:00:00 2001 From: Harshil Agrawal Date: Wed, 14 Oct 2026 12:23:29 +0200 Subject: [PATCH 5/5] Address alarm review feedback: jittered backoff and timing wording - Rules of Durable Objects: add jitter to the backoff in the good example, as suggested in review, so retries from objects that fail at the same time are spread out. - Reword "back off on failure" to "back off for retried operations" to distinguish handler-scheduled retries from exceptions escaping alarm(), which have their own built-in backoff. Applied to the Rules bullet, the Rules intro, and the Alarms caution. - Reword "runs (again) immediately" to "is (re)scheduled to run immediately", since alarms are not guaranteed to run at an exact time. Applied to the Alarms caution, the Rules intro, the setAlarm bullet, and the storage partial. Verified: example and tests type-check; good example passes; the setAlarm(Date.now()) and no-deadline variants fail the intended tests. pnpm run check and pnpm run build pass. --- src/content/docs/durable-objects/api/alarms.mdx | 6 +++--- .../best-practices/rules-of-durable-objects.mdx | 12 +++++++----- .../partials/durable-objects/api-storage-alarms.mdx | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/content/docs/durable-objects/api/alarms.mdx b/src/content/docs/durable-objects/api/alarms.mdx index e13a4732140..8b0440a9b1e 100644 --- a/src/content/docs/durable-objects/api/alarms.mdx +++ b/src/content/docs/durable-objects/api/alarms.mdx @@ -21,9 +21,9 @@ Notably: - 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 runs again 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. +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 after failures, and stop at a deadline. +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). ::: @@ -101,7 +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 runs 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). + - 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. diff --git a/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx b/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx index c9f253c464a..95a81e1f6f0 100644 --- a/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx +++ b/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx @@ -1486,7 +1486,7 @@ This pattern does not scale. As traffic increases, the single Durable Object bec ### 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 runs again immediately. If the condition that caused the reschedule does not change, the handler reschedules itself again. This creates an alarm loop. +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. @@ -1501,7 +1501,7 @@ Compute duration stays relatively low, because a looping object is billed as one 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 on failure.** Increase the delay after each failed attempt instead of retrying at a fixed short interval. +- **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). @@ -1547,7 +1547,7 @@ export class Session extends DurableObject { ``` -Instead, enforce a minimum delay, back off on failure, and stop at a deadline. The following example derives both the backoff and the deadline from `refreshAt`, so retries need no extra storage writes: +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: ```ts @@ -1594,8 +1594,10 @@ export class Session extends DurableObject { return; } - // Back off: wait about as long as the refresh has been failing - await this.rescheduleAlarm(Date.now() + Math.min(overdueMs, MAX_DELAY_MS)); + // 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 diff --git a/src/content/partials/durable-objects/api-storage-alarms.mdx b/src/content/partials/durable-objects/api-storage-alarms.mdx index ef683dd6a91..0d6e627faba 100644 --- a/src/content/partials/durable-objects/api-storage-alarms.mdx +++ b/src/content/partials/durable-objects/api-storage-alarms.mdx @@ -18,7 +18,7 @@ import { Type, MetaInfo, Render } from "~/components"; - setAlarm(scheduledTime , options ): - 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. If you call `setAlarm()` this way from inside `alarm()`, the handler runs again 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. + 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`