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&lt;Subscription>;
}

export class Subscription extends DurableObject&lt;Env> {
    async alarm() {
        // ✅ Good: Check state before performing the action
        const lastRenewal = await this.ctx.storage.get&lt;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&lt;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&lt;ChatRoom>;
}

export class ChatRoom extends DurableObject&lt;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&lt;RateLimiter>;
}

// 🔴 Bad: Global rate limiter - ALL requests go through one instance
export class RateLimiter extends DurableObject&lt;Env> {
    async checkLimit(ip: string): Promise&lt;boolean> {
        const key = `rate:${ip}`;
        const count = (await this.ctx.storage.get&lt;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&lt;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.

  • 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

Rules of Durable Objects·续4 - 图1Docs

{"@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


Skip to content

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.