Rules of Durable Objects·续4
// Schedule the next alarm only if there's more work to do // In this case, schedule cleanup in 24 hours await this.ctx.storage.setAlarm(Date.now() + 24 * 60 * 60 * 1000); }
async calculateFinalScores() { // Game ending logic } }
*index.tsts*
```ts
import { DurableObject } from "cloudflare:workers";
export interface Env {
GAME_MATCH: DurableObjectNamespace<GameMatch>;
}
export class GameMatch extends DurableObject<Env> {
async startGame(durationMs: number = 60000) {
await this.ctx.storage.put("gameStarted", Date.now());
await this.ctx.storage.put("gameActive", true);
// Schedule the game to end after the duration
await this.ctx.storage.setAlarm(Date.now() + durationMs);
}
// Called when the alarm fires
async alarm(alarmInfo?: AlarmInvocationInfo) {
const isActive = await this.ctx.storage.get<boolean>("gameActive");
if (!isActive) {
return; // Game was already ended
}
// End the game
await this.ctx.storage.put("gameActive", false);
await this.ctx.storage.put("gameEnded", Date.now());
// Calculate final scores, notify players, etc.
try {
await this.calculateFinalScores();
} catch (err) {
// If we're almost out of retries but still have work to do, schedule a new alarm
// rather than letting our retries run out to ensure we keep getting invoked.
if (alarmInfo && alarmInfo.retryCount >= 5) {
await this.ctx.storage.setAlarm(Date.now() + 30 * 1000);
return;
}
throw err;
}
// Schedule the next alarm only if there's more work to do
// In this case, schedule cleanup in 24 hours
await this.ctx.storage.setAlarm(Date.now() + 24 * 60 * 60 * 1000);
}
private async calculateFinalScores() {
// Game ending logic
}
}
Make alarm handlers idempotent
In rare cases, alarms may fire more than once. Your alarm() handler should be safe to run multiple times without causing issues.
index.jsjs
import { DurableObject } from "cloudflare:workers";
export class Subscription extends DurableObject {
async alarm() {
// ✅ Good: Check state before performing the action
const lastRenewal = await this.ctx.storage.get("lastRenewal");
const renewalPeriod = 30 * 24 * 60 * 60 * 1000; // 30 days
// If we already renewed recently, don't do it again
if (lastRenewal && Date.now() - lastRenewal < renewalPeriod - 60000) {
console.log("Already renewed recently, skipping");
return;
}
// Perform the renewal
const success = await this.processRenewal();
if (success) {
// Record the renewal time
await this.ctx.storage.put("lastRenewal", Date.now());
// Schedule the next renewal
await this.ctx.storage.setAlarm(Date.now() + renewalPeriod);
} else {
// Retry in 1 hour
await this.ctx.storage.setAlarm(Date.now() + 60 * 60 * 1000);
}
}
async processRenewal() {
// Payment processing logic
return true;
}
}
index.tsts
import { DurableObject } from "cloudflare:workers";
export interface Env {
SUBSCRIPTION: DurableObjectNamespace<Subscription>;
}
export class Subscription extends DurableObject<Env> {
async alarm() {
// ✅ Good: Check state before performing the action
const lastRenewal = await this.ctx.storage.get<number>("lastRenewal");
const renewalPeriod = 30 * 24 * 60 * 60 * 1000; // 30 days
// If we already renewed recently, don't do it again
if (lastRenewal && Date.now() - lastRenewal < renewalPeriod - 60000) {
console.log("Already renewed recently, skipping");
return;
}
// Perform the renewal
const success = await this.processRenewal();
if (success) {
// Record the renewal time
await this.ctx.storage.put("lastRenewal", Date.now());
// Schedule the next renewal
await this.ctx.storage.setAlarm(Date.now() + renewalPeriod);
} else {
// Retry in 1 hour
await this.ctx.storage.setAlarm(Date.now() + 60 * 60 * 1000);
}
}
private async processRenewal(): Promise<boolean> {
// Payment processing logic
return true;
}
}
Clean up storage with deleteAll()
To fully clear a Durable Object's storage, call deleteAll(). Simply deleting individual keys or dropping tables is not sufficient, as some internal metadata may remain. Workers with a compatibility date before 2026-02-24 and an alarm set should delete the alarm first with deleteAlarm().
index.jsjs
import { DurableObject } from "cloudflare:workers";
export class ChatRoom extends DurableObject {
async clearStorage() {
// Delete all storage, including any set alarm
await this.ctx.storage.deleteAll();
// The Durable Object instance still exists, but with empty storage
// A subsequent request will find no data
}
}
index.tsts
import { DurableObject } from "cloudflare:workers";
export interface Env {
CHAT_ROOM: DurableObjectNamespace<ChatRoom>;
}
export class ChatRoom extends DurableObject<Env> {
async clearStorage() {
// Delete all storage, including any set alarm
await this.ctx.storage.deleteAll();
// The Durable Object instance still exists, but with empty storage
// A subsequent request will find no data
}
}
Design for unexpected shutdowns
Durable Objects may shut down at any time due to deployments, inactivity, or runtime decisions. Rather than relying on shutdown hooks (which are not provided), design your application to write state incrementally.
Shutdown hooks or lifecycle callbacks that run before shutdown are not provided because Cloudflare cannot guarantee these hooks would execute in all cases, and external software may rely too heavily on these (unreliable) hooks.
Instead of relying on shutdown hooks, you can regularly write to storage to recover gracefully from shutdowns.
For example, if you are processing a stream of data and need to save your progress, write your position to storage as you go rather than waiting to persist it at the end:
// Good: Write progress as you go
async processData(data) {
data.forEach(async (item, index) => {
await this.processItem(item);
// Save progress frequently
await this.ctx.storage.put("lastProcessedIndex", index);
});
}
While this may feel unintuitive, Durable Object storage writes are fast and synchronous, so you can persist state with minimal performance concerns.
This approach ensures your Durable Object can safely resume from any point, even if it shuts down unexpectedly.
Anti-patterns to avoid
Do not use a single Durable Object as a global singleton
A single Durable Object handling all traffic becomes a bottleneck. While async operations allow request interleaving, all synchronous JavaScript execution is single-threaded, and storage operations provide serialization guarantees that limit throughput.
A common mistake is using a Durable Object for global rate limiting or global counters. This funnels all traffic through a single instance:
index.jsjs
import { DurableObject } from "cloudflare:workers";
// 🔴 Bad: Global rate limiter - ALL requests go through one instance
export class RateLimiter extends DurableObject {
async checkLimit(ip) {
const key = `rate:${ip}`;
const count = (await this.ctx.storage.get(key)) ?? 0;
await this.ctx.storage.put(key, count + 1);
return count < 100;
}
}
// 🔴 Bad: Always using the same ID creates a global bottleneck
export default {
async fetch(request, env) {
// Every single request to your application goes through this one DO
const limiter = env.RATE_LIMITER.get(env.RATE_LIMITER.idFromName("global"));
const ip = request.headers.get("CF-Connecting-IP") ?? "unknown";
const allowed = await limiter.checkLimit(ip);
if (!allowed) {
return new Response("Rate limited", { status: 429 });
}
return new Response("OK");
},
};
index.tsts
import { DurableObject } from "cloudflare:workers";
export interface Env {
RATE_LIMITER: DurableObjectNamespace<RateLimiter>;
}
// 🔴 Bad: Global rate limiter - ALL requests go through one instance
export class RateLimiter extends DurableObject<Env> {
async checkLimit(ip: string): Promise<boolean> {
const key = `rate:${ip}`;
const count = (await this.ctx.storage.get<number>(key)) ?? 0;
await this.ctx.storage.put(key, count + 1);
return count < 100;
}
}
// 🔴 Bad: Always using the same ID creates a global bottleneck
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Every single request to your application goes through this one DO
const limiter = env.RATE_LIMITER.get(
env.RATE_LIMITER.idFromName("global")
);
const ip = request.headers.get("CF-Connecting-IP") ?? "unknown";
const allowed = await limiter.checkLimit(ip);
if (!allowed) {
return new Response("Rate limited", { status: 429 });
}
return new Response("OK");
},
};
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.
Testing and class lifecycle
Test with Vitest and plan for class lifecycle changes
Use @cloudflare/vitest-plugin for testing Durable Objects. The integration provides utilities for direct instance access.
test/chat-room.test.jsjs
import { env } from "cloudflare:workers";
import { runInDurableObject, runDurableObjectAlarm } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("ChatRoom", () => {
it("should send and retrieve messages", async () => {
const id = env.CHAT_ROOM.idFromName("test-room");
const stub = env.CHAT_ROOM.get(id);
// Call RPC methods directly on the stub
await stub.sendMessage("user-1", "Hello!");
await stub.sendMessage("user-2", "Hi there!");
const messages = await stub.getMessages(10);
expect(messages).toHaveLength(2);
});
it("can access instance internals and trigger alarms", async () => {
const id = env.CHAT_ROOM.idFromName("test-room");
const stub = env.CHAT_ROOM.get(id);
// Access storage directly for verification
await runInDurableObject(stub, async (instance, state) => {
const count = state.storage.sql
.exec("SELECT COUNT(*) as count FROM messages")
.one();
expect(count.count).toBe(2);
});
// Trigger alarms immediately without waiting
const alarmRan = await runDurableObjectAlarm(stub);
expect(alarmRan).toBe(false); // No alarm was scheduled
});
});
test/chat-room.test.tsts
import { env } from "cloudflare:workers";
import {
runInDurableObject,
runDurableObjectAlarm,
} from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("ChatRoom", () => {
it("should send and retrieve messages", async () => {
const id = env.CHAT_ROOM.idFromName("test-room");
const stub = env.CHAT_ROOM.get(id);
// Call RPC methods directly on the stub
await stub.sendMessage("user-1", "Hello!");
await stub.sendMessage("user-2", "Hi there!");
const messages = await stub.getMessages(10);
expect(messages).toHaveLength(2);
});
it("can access instance internals and trigger alarms", async () => {
const id = env.CHAT_ROOM.idFromName("test-room");
const stub = env.CHAT_ROOM.get(id);
// Access storage directly for verification
await runInDurableObject(stub, async (instance, state) => {
const count = state.storage.sql
.exec<{ count: number }>("SELECT COUNT(*) as count FROM messages")
.one();
expect(count.count).toBe(2);
});
// Trigger alarms immediately without waiting
const alarmRan = await runDurableObjectAlarm(stub);
expect(alarmRan).toBe(false); // No alarm was scheduled
});
});
Configure Vitest in your vitest.config.ts:
import { cloudflareTest } from "@cloudflare/vitest-plugin";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [
cloudflareTest({
wrangler: { configPath: "./wrangler.jsonc" },
}),
],
});
For data-schema changes, run schema migrations in the constructor using blockConcurrencyWhile(). For class renames or deletions, change the class entry in the exports field of your Wrangler configuration file:
{
"exports": {
// Rename a class — also add a live entry for the new name
"OldChatRoom": { "type": "durable-object", "state": "renamed", "renamed_to": "ChatRoom" },
"ChatRoom": { "type": "durable-object", "storage": "sqlite" },
// Delete a class (removes all data!)
"DeprecatedRoom": { "type": "durable-object", "state": "deleted" }
}
}
[exports.OldChatRoom]
type = "durable-object"
state = "renamed"
renamed_to = "ChatRoom"
[exports.ChatRoom]
type = "durable-object"
storage = "sqlite"
[exports.DeprecatedRoom]
type = "durable-object"
state = "deleted"
Refer to Durable Object class exports for more details on class lifecycle changes, and Testing with Durable Objects for comprehensive testing patterns including SQLite queries and alarm testing.
Related resources
- Workers Best Practices: code patterns for request handling, observability, and security that apply to the Workers calling your Durable Objects.
- Rules of Workflows: best practices for durable, multi-step Workflows — useful when combining Workflows with Durable Objects for long-running orchestration.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/durable-objects/best-practices/rules-of-durable-objects/#page","headline":"Rules of Durable Objects","description":"Design guidelines for building correct and effective Durable Objects applications, covering when and how to use them.","url":"https://developers.cloudflare.com/durable-objects/best-practices/rules-of-durable-objects/","inLanguage":"en","image":"https://developers.cloudflare.com/durable-objects/best-practices/rules-of-durable-objects/og.png?v=238433fba2f60d80","dateModified":"2026-10-18","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Serve WebSocket connections from Durable Objects, including the standard and Hibernation APIs. title: Use WebSockets image: https://developers.cloudflare.com/durable-objects/best-practices/websockets/og.png?v=f6ee405dcdefa97c
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/durable-objects/llms.txt
Use this file to discover all available pages before exploring further.
Docs