Skip to content
sproutboatDocs
Esc
↑↓navigate↵open⌘Jpreview
On this page

Durable Objects

Per-name serialized state, RPC methods and alarms, as on Cloudflare Durable Objects.

Configuration

"durable_objects": { "COUNTER": "Counter" }

The binding name maps to a class the entry file exports. Use compatibility date 2026-10-29 or later for the form below.

Usage

A Durable Object is a class that extends DurableObject. Its public methods are called over RPC, and this.ctx.storage is its own storage:

import { DurableObject } from "cloudflare:workers";

export class Counter extends DurableObject<Env> {
  async increment(): Promise<number> {
    const n = ((await this.ctx.storage.get<number>("n")) ?? 0) + 1;
    await this.ctx.storage.put("n", n);
    // Debounced: a burst of calls produces one alarm, not one per call.
    await this.ctx.storage.setAlarm(Date.now() + 1000);
    return n;
  }

  // Runs later, with no request in flight.
  async alarm() {
    console.log("counter settled at", await this.ctx.storage.get("n"));
  }
}

export default {
  async fetch(request, env, ctx) {
    const counter = env.COUNTER.getByName("global");
    return Response.json({ n: await counter.increment() });
  },
} satisfies ExportedHandler<Env>;

getByName(name) is get(idFromName(name)). The stub forwards a request too: await stub.fetch(request) calls the class’s fetch(request) method, as on Workers. sproutboat types types env.COUNTER with the class, so counter.increment() is checked. See Generated types.

Storage and alarms

this.ctx.storage has get, put, delete, deleteAll, list({ prefix?, limit?, start?, end?, reverse? }) (a Map), transaction(), setAlarm(time), getAlarm() and deleteAlarm(), all returning Promises. Values are copied as structured clone does, so Date, Map, Set, BigInt and typed arrays round-trip. Each object has at most one pending alarm; a later setAlarm replaces it. ctx.storage.sql.exec(query, ...bindings) runs SQL against the object’s own SQLite database, and ctx.waitUntil(promise) works as in a handler.

While a storage read is in flight, no other call or alarm enters the object, so await storage.get("n") followed by await storage.put("n", n + 1) cannot interleave with another call. An event that awaits something else, a fetch or a timer, lets the next one in, as on Workers; use ctx.blockConcurrencyWhile() or transaction() to hold the gate.

The namespace has getByName(name), idFromName(name), idFromString(hex), newUniqueId() and get(id). Ids are 64 hex digits.

Before compatibility date 2026-10-29

Earlier dates keep an older, synchronous form. Storage calls are blocking, so there is no await, the class does not extend anything, a method is not callable over RPC, and the Worker reaches the object with get(id).fetch(request):

class Counter {
  constructor(state, env) { this.state = state; this.env = env; }
  fetch(request) {
    const n = (this.state.storage.get("n") || 0) + 1;
    this.state.storage.put("n", n);
    return Response.json({ n });
  }
}
export default {
  fetch(request, env) {
    const id = env.COUNTER.idFromName("global");
    return env.COUNTER.get(id).fetch(request);
  },
};

From 2026-10-28, storage and stub.fetch() return Promises (with the input gate above); from 2026-10-29 a class that extends DurableObject is called over RPC. await works at every date, so a handler that awaits its storage calls is correct on both sides of each.

Was this page helpful?