How to Configure Durable Object Alarm Scheduling in workerd

Durable Object alarms in workerd are configured by implementing an alarm() method in your DO class and calling ctx.storage.setAlarm(timestamp) to persist the schedule to SQLite, where the internal AlarmScheduler manages execution with automatic retries and jitter.

The workerd runtime enables Durable Objects (DOs) to schedule future alarm() invocations that execute independently of client requests. This mechanism relies on a three-layer architecture spanning storage APIs, an in-process scheduler, and the DO interface itself.

Architecture Overview

Understanding how workerd handles alarms requires examining three distinct layers in the source code.

Storage-Level API

The ctx.storage.setAlarm(msSinceEpoch) method writes the alarm time into the per-object SQLite metadata. This implementation resides in src/workerd/io/actor-sqlite.c++ (see setAlarm() at lines 740-749), which delegates persistence to src/workerd/util/sqlite-metadata.c++ (lines 23-31).

In-Process Scheduler

The AlarmScheduler class manages pending alarms in memory, loading them from SQLite on startup and creating kj::Promise objects that fire at the scheduled time. It handles jitter and exponential back-off for retries. Key definitions appear in src/workerd/server/alarm-scheduler.h (lines 43-70) with implementation logic in src/workerd/server/alarm-scheduler.c++ (lines 104-121).

Durable Object Interface

A DO declares an optional alarm(alarmInfo?) method that the scheduler invokes when the alarm fires. The optional alarmInfo argument contains retry metadata including isRetry and retryCount. This interface is defined in types/defines/rpc.d.ts (lines 61-63).

Implementation Steps

Follow these steps to configure alarm scheduling in your Durable Object.

1. Declare the Alarm Handler

Add an async alarm(alarmInfo?: AlarmInvocationInfo) method to your DO class. You may also use alarm() without parameters if retry handling is not required.

2. Schedule the Alarm

Inside any request handler or method, call await this.ctx.storage.setAlarm(Date.now() + delayMs). The parameter represents milliseconds since epoch (Unix timestamp), allowing any future point in time.

3. Cancel Scheduled Alarms

To clear a pending alarm, invoke await this.ctx.storage.setAlarm(null). This removes the entry from SQLite and cancels the in-memory scheduled task.

4. Handle Retry Logic

Examine the alarmInfo object to determine if the alarm is a retry (alarmInfo.isRetry) and how many retries have occurred (alarmInfo.retryCount). Use this data to implement idempotent logic that safely handles repeated executions.

5. Persistence Across Restarts

The alarm time is stored in the DO's SQLite metadata (alarms.sqlite). On worker startup, AlarmScheduler::loadAlarmsFromDb() (defined at lines 60-68 in src/workerd/server/alarm-scheduler.c++) automatically restores all pending alarms.

Code Examples

TypeScript/JavaScript Implementation

// src/my-do.ts
export class CounterDO {
  // ctx is a DurableObjectState injected by workerd
  constructor(private readonly ctx: DurableObjectState) {}

  // Optional alarm handler – receives retry info if the alarm is being retried
  async alarm(alarmInfo?: AlarmInvocationInfo) {
    // Example: increment a counter stored in durable storage
    const val = (await this.ctx.storage.get<number>('count')) ?? 0;
    await this.ctx.storage.put('count', val + 1);
    console.log('Alarm fired', {retry: alarmInfo?.isRetry});
  }

  // Regular fetch handler – schedules an alarm 30 seconds from now
  async fetch(request: Request) {
    const delay = 30_000; // 30s
    await this.ctx.storage.setAlarm(Date.now() + delay);
    return new Response('Alarm scheduled');
  }

  // Optional helper to cancel any pending alarm
  async cancelAlarm() {
    await this.ctx.storage.setAlarm(null);
  }
}

Python Implementation


# src/workerd/server/tests/python/durable-object/worker.py

class CounterDO:
    def __init__(self, ctx):
        self.ctx = ctx
        self.alarm_triggered = False

    async def fetch(self, request):
        # schedule alarm 100 ms in the future

        await self.ctx.storage.setAlarm(Date.now() + 100)
        return Response('OK')

    async def alarm(self, alarm_info):
        self.alarm_triggered = True

C++ Internals

The following excerpt from src/workerd/server/alarm-scheduler.c++ demonstrates how the scheduler persists alarms to SQLite before updating in-memory state:

// src/workerd/server/alarm-scheduler.c++ (excerpt)
bool AlarmScheduler::setAlarm(ActorKey actor, kj::Date scheduledTime) {
  // Persist to SQLite first; if DB write fails we abort the in-memory update.
  if (!db->runTxn([&](SqliteTransaction& txn) {
        return txn.exec(stmtSetAlarm, actor.uniqueKey, actor.actorId, scheduledTime);
      })) {
    return false;
  }
  // Update in-memory map and (re)queue the task.
  setAlarmInMemory(kj::heap<ActorKey>(actor), scheduledTime);
  return true;
}

Summary

  • Work Flow: Call ctx.storage.setAlarm(timestamp) to write to SQLite, then AlarmScheduler manages the execution timeline.
  • Persistence: Alarms survive process restarts because they are stored in the DO's SQLite metadata and reloaded via loadAlarmsFromDb().
  • Cancellation: Pass null to setAlarm() to clear pending alarms.
  • Retries: The alarmInfo parameter provides isRetry and retryCount for implementing idempotent alarm handlers.
  • Source Files: Key implementations reside in src/workerd/io/actor-sqlite.c++, src/workerd/server/alarm-scheduler.c++, and types/defines/rpc.d.ts.

Frequently Asked Questions

How do I cancel a scheduled alarm in workerd?

Call await this.ctx.storage.setAlarm(null) from within your Durable Object. According to the implementation in src/workerd/io/actor-sqlite.c++, passing null (or kj::none in C++) clears the alarm entry from SQLite metadata and triggers the scheduler to remove the pending task from its in-memory queue.

What happens to scheduled alarms when the worker restarts?

Scheduled alarms persist automatically. The AlarmScheduler::loadAlarmsFromDb() method (implemented in src/workerd/server/alarm-scheduler.c++) executes on startup, reading all alarm entries from the SQLite database and reconstructing the internal scheduling queue. This ensures alarms survive deployments and process restarts without additional configuration.

How does retry logic work for failed alarms?

If an alarm() handler throws an exception or the process crashes during execution, the AlarmScheduler automatically reschedules the alarm with exponential back-off and jitter. When the retry fires, the scheduler passes an AlarmInvocationInfo object to your handler with isRetry: true and an incremented retryCount, allowing you to detect retries and implement appropriate back-off strategies or circuit breakers.

Can I schedule multiple alarms for the same Durable Object?

No, the current implementation in workerd supports only one scheduled alarm per Durable Object instance. Calling setAlarm() multiple times overwrites the previous alarm time in SQLite. If you need multiple future events, store an array of timestamps in durable storage and schedule the next imminent alarm, rescheduling subsequent alarms within your alarm() handler as each fires.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →