Rules of Durable Objects·续1

// Initialize the child Durable Object const childId = this.env.GAME_MATCH.idFromName(matchId); const childStub = this.env.GAME_MATCH.get(childId); await childStub.init(matchId, matchName);

return matchId; }

async listMatches(): Promise<{ id: string; name: string }[]> { // Parent knows about all matches without waking up each child const cursor = this.ctx.storage.sql.exec<{ id: string; name: string }>( "SELECT id, name FROM matches ORDER BY created_at DESC" ); return cursor.toArray(); } }

// Child: Handles its own game state independently export class GameMatch extends DurableObject<Env> { async init(matchId: string, matchName: string) { await this.ctx.storage.put("matchId", matchId); await this.ctx.storage.put("matchName", matchName); this.ctx.storage.sql.exec(CREATE TABLE IF NOT EXISTS players ( id TEXT PRIMARY KEY, name TEXT NOT NULL, score INTEGER DEFAULT 0 )); }

async addPlayer(playerId: string, playerName: string) { this.ctx.storage.sql.exec( "INSERT INTO players (id, name, score) VALUES (?, ?, 0)", playerId, playerName ); }

async updateScore(playerId: string, score: number) { this.ctx.storage.sql.exec( "UPDATE players SET score = ? WHERE id = ?", score, playerId ); } }


With this pattern:

- Listing matches only queries the parent (children stay hibernated)
- Different matches process player actions in parallel
- Each match has its own SQLite database for player data

### Consider location hints for latency-sensitive applications

By default, a Durable Object is created near the location of the first request it receives. For most applications, this works well. However, you can provide a location hint to influence where the Durable Object is created.

*index.jsjs*

```js
import { DurableObject } from "cloudflare:workers";

export class GameSession extends DurableObject {
    // Game session logic
}

export default {
    async fetch(request, env) {
        const url = new URL(request.url);
        const gameId = url.searchParams.get("game") ?? "default";
        const region = url.searchParams.get("region") ?? "wnam"; // Western North America

// Provide a location hint for where this Durable Object should be created
        const id = env.GAME_SESSION.idFromName(gameId);
        const stub = env.GAME_SESSION.get(id, { locationHint: region });

return new Response("Connected to game session");
    },
};

index.tsts

import { DurableObject } from "cloudflare:workers";

export interface Env {
    GAME_SESSION: DurableObjectNamespace&lt;GameSession>;
}

export class GameSession extends DurableObject&lt;Env> {
    // Game session logic
}

export default {
    async fetch(request: Request, env: Env): Promise&lt;Response> {
        const url = new URL(request.url);
        const gameId = url.searchParams.get("game") ?? "default";
        const region = url.searchParams.get("region") ?? "wnam"; // Western North America

// Provide a location hint for where this Durable Object should be created
        const id = env.GAME_SESSION.idFromName(gameId);
        const stub = env.GAME_SESSION.get(id, { locationHint: region });

return new Response("Connected to game session");
    },
};

Location hints are suggestions, not guarantees. Refer to Data location for available regions and details.

Storage and state

Use SQLite-backed Durable Objects

SQLite storage is the recommended storage backend for new Durable Objects. It provides a familiar SQL API for relational queries, indexes, transactions, and better performance than the legacy key-value storage backed Durable Objects. SQLite Durable Objects also support the KV API in synchronous and asynchronous versions.

Configure your Durable Object class to use SQLite storage in your Wrangler configuration:

{
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["ChatRoom"] }
  ]
}
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "ChatRoom" ]

Then use the SQL API in your Durable Object:

index.jsjs

import { DurableObject } from "cloudflare:workers";

export class ChatRoom extends DurableObject {
    constructor(ctx, env) {
        super(ctx, env);

// Create tables on first instantiation
        this.ctx.storage.sql.exec(`
            CREATE TABLE IF NOT EXISTS messages (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id TEXT NOT NULL,
                content TEXT NOT NULL,
                created_at INTEGER NOT NULL
            )
        `);
    }

async addMessage(userId, content) {
        this.ctx.storage.sql.exec(
            "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",
            userId,
            content,
            Date.now(),
        );
    }

async getRecentMessages(limit = 50) {
        // Use type parameter for typed results
        const cursor = this.ctx.storage.sql.exec(
            "SELECT * FROM messages ORDER BY created_at DESC LIMIT ?",
            limit,
        );
        return cursor.toArray();
    }
}

index.tsts

import { DurableObject } from "cloudflare:workers";

export interface Env {
    CHAT_ROOM: DurableObjectNamespace&lt;ChatRoom>;
}

type Message = {
id: number;
user_id: string;
content: string;
created_at: number;
};

export class ChatRoom extends DurableObject&lt;Env> {
    constructor(ctx: DurableObjectState, env: Env) {
        super(ctx, env);

// Create tables on first instantiation
        this.ctx.storage.sql.exec(`
            CREATE TABLE IF NOT EXISTS messages (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id TEXT NOT NULL,
                content TEXT NOT NULL,
                created_at INTEGER NOT NULL
            )
        `);
    }

async addMessage(userId: string, content: string) {
        this.ctx.storage.sql.exec(
            "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",
            userId,
            content,
            Date.now()
        );
    }

async getRecentMessages(limit: number = 50): Promise&lt;Message[]> {
        // Use type parameter for typed results
        const cursor = this.ctx.storage.sql.exec&lt;Message>(
            "SELECT * FROM messages ORDER BY created_at DESC LIMIT ?",
            limit
        );
        return cursor.toArray();
    }
}

Refer to Access Durable Objects storage for more details on the SQL API.

Initialize storage and run migrations in the constructor

Use blockConcurrencyWhile() in the constructor to run migrations and initialize state before any requests are processed. This ensures your schema is ready and prevents race conditions during initialization.

Note

PRAGMA user_version is not supported by Durable Objects SQLite storage. You must use an alternative approach to track your schema version.

For production applications, use a migration library that handles version tracking and execution automatically:

If you prefer not to use a library, you can track schema versions manually using a _sql_schema_migrations table. The following example demonstrates this approach:

index.jsjs

import { DurableObject } from "cloudflare:workers";

export class ChatRoom extends DurableObject {
    constructor(ctx, env) {
        super(ctx, env);

// blockConcurrencyWhile() ensures no requests are processed until this completes
        ctx.blockConcurrencyWhile(async () => {
            await this.migrate();
        });
    }

async migrate() {
        // Create the migrations tracking table if it does not exist
        this.ctx.storage.sql.exec(`
            CREATE TABLE IF NOT EXISTS _sql_schema_migrations (
                id INTEGER PRIMARY KEY,
                applied_at TEXT NOT NULL DEFAULT (datetime('now'))
            );
        `);

// Determine the current schema version
        const version = this.ctx.storage.sql
            .exec(
                "SELECT COALESCE(MAX(id), 0) as version FROM _sql_schema_migrations",
            )
            .one().version;

if (version < 1) {
            this.ctx.storage.sql.exec(`
                CREATE TABLE IF NOT EXISTS messages (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    user_id TEXT NOT NULL,
                    content TEXT NOT NULL,
                    created_at INTEGER NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
                INSERT INTO _sql_schema_migrations (id) VALUES (1);
            `);
        }

if (version < 2) {
            // Future migration: add a new column
            this.ctx.storage.sql.exec(`
                ALTER TABLE messages ADD COLUMN edited_at INTEGER;
                INSERT INTO _sql_schema_migrations (id) VALUES (2);
            `);
        }
    }
}

index.tsts

import { DurableObject } from "cloudflare:workers";

export interface Env {
    CHAT_ROOM: DurableObjectNamespace&lt;ChatRoom>;
}

export class ChatRoom extends DurableObject&lt;Env> {
    constructor(ctx: DurableObjectState, env: Env) {
        super(ctx, env);

// blockConcurrencyWhile() ensures no requests are processed until this completes
        ctx.blockConcurrencyWhile(async () => {
            await this.migrate();
        });
    }

private async migrate() {
        // Create the migrations tracking table if it does not exist
        this.ctx.storage.sql.exec(`
            CREATE TABLE IF NOT EXISTS _sql_schema_migrations (
                id INTEGER PRIMARY KEY,
                applied_at TEXT NOT NULL DEFAULT (datetime('now'))
            );
        `);

// Determine the current schema version
        const version =
            this.ctx.storage.sql
                .exec<{ version: number }>(
                    "SELECT COALESCE(MAX(id), 0) as version FROM _sql_schema_migrations",
                )
                .one().version;

if (version < 1) {
            this.ctx.storage.sql.exec(`
                CREATE TABLE IF NOT EXISTS messages (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    user_id TEXT NOT NULL,
                    content TEXT NOT NULL,
                    created_at INTEGER NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
                INSERT INTO _sql_schema_migrations (id) VALUES (1);
            `);
        }

if (version < 2) {
            // Future migration: add a new column
            this.ctx.storage.sql.exec(`
                ALTER TABLE messages ADD COLUMN edited_at INTEGER;
                INSERT INTO _sql_schema_migrations (id) VALUES (2);
            `);
        }
    }
}

Understand the difference between in-memory state and persistent storage

Durable Objects provide multiple state management layers, each with different characteristics:

Type Speed Persistence Use Case
In-memory (class properties) Fastest Lost on eviction or crash Caching, active connections
SQLite storage Fast Durable across restarts Primary data storage
External (R2, D1) Variable Durable, cross-DO accessible Large files, shared data

In-memory state is not preserved if the Durable Object is evicted from memory due to inactivity, or if it crashes from an uncaught exception. Always persist important state to SQLite storage.

index.jsjs

import { DurableObject } from "cloudflare:workers";

export class ChatRoom extends DurableObject {
    // In-memory cache - fast but NOT preserved across evictions or crashes
    messageCache = null;

async getRecentMessages() {
        // Return from cache if available (only valid while DO is in memory)
        if (this.messageCache !== null) {
            return this.messageCache;
        }

// Otherwise, load from durable storage
        const cursor = this.ctx.storage.sql.exec(
            "SELECT * FROM messages ORDER BY created_at DESC LIMIT 100",
        );
        this.messageCache = cursor.toArray();
        return this.messageCache;
    }

async addMessage(userId, content) {
        // ✅ Always persist to durable storage first
        this.ctx.storage.sql.exec(
            "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",
            userId,
            content,
            Date.now(),
        );

// Then update the cache (if it exists)
        // If the DO crashes here, the message is still saved in SQLite
        this.messageCache = null; // Invalidate cache
    }
}

index.tsts

import { DurableObject } from "cloudflare:workers";

export interface Env {
    CHAT_ROOM: DurableObjectNamespace&lt;ChatRoom>;
}

type Message = {
id: number;
user_id: string;
content: string;
created_at: number;
};

export class ChatRoom extends DurableObject&lt;Env> {
    // In-memory cache - fast but NOT preserved across evictions or crashes
    private messageCache: Message[] | null = null;

async getRecentMessages(): Promise&lt;Message[]> {
        // Return from cache if available (only valid while DO is in memory)
        if (this.messageCache !== null) {
            return this.messageCache;
        }

// Otherwise, load from durable storage
        const cursor = this.ctx.storage.sql.exec&lt;Message>(
            "SELECT * FROM messages ORDER BY created_at DESC LIMIT 100"
        );
        this.messageCache = cursor.toArray();
        return this.messageCache;
    }

async addMessage(userId: string, content: string) {
        // ✅ Always persist to durable storage first
        this.ctx.storage.sql.exec(
            "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",
            userId,
            content,
            Date.now()
        );

// Then update the cache (if it exists)
        // If the DO crashes here, the message is still saved in SQLite
        this.messageCache = null; // Invalidate cache
    }
}

Caution

If an uncaught exception occurs in your Durable Object, the runtime may terminate the instance. Any in-memory state will be lost, but SQLite storage remains intact. Always persist critical state to storage before performing operations that might fail.

Create indexes for frequently-queried columns

Just like any database, indexes dramatically improve read performance for frequently-filtered columns. The cost is slightly more storage and marginally slower writes.

index.jsjs

import { DurableObject } from "cloudflare:workers";

export class ChatRoom extends DurableObject {
    constructor(ctx, env) {
        super(ctx, env);

ctx.blockConcurrencyWhile(async () => {
            this.ctx.storage.sql.exec(`
                CREATE TABLE IF NOT EXISTS messages (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    user_id TEXT NOT NULL,
                    content TEXT NOT NULL,
                    created_at INTEGER NOT NULL
                );

-- Index for queries filtering by user
                CREATE INDEX IF NOT EXISTS idx_messages_user_id ON messages(user_id);

-- Index for time-based queries (recent messages)
                CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);

-- Composite index for user + time queries
                CREATE INDEX IF NOT EXISTS idx_messages_user_time ON messages(user_id, created_at);
            `);
        });
    }

// This query benefits from idx_messages_user_time
    async getUserMessages(userId, since) {
        return this.ctx.storage.sql
            .exec(
                "SELECT * FROM messages WHERE user_id = ? AND created_at > ? ORDER BY created_at",
                userId,
                since,
            )
            .toArray();
    }
}

index.tsts

import { DurableObject } from "cloudflare:workers";

export interface Env {
    CHAT_ROOM: DurableObjectNamespace&lt;ChatRoom>;
}

export class ChatRoom extends DurableObject&lt;Env> {
    constructor(ctx: DurableObjectState, env: Env) {
        super(ctx, env);

ctx.blockConcurrencyWhile(async () => {
            this.ctx.storage.sql.exec(`
                CREATE TABLE IF NOT EXISTS messages (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    user_id TEXT NOT NULL,
                    content TEXT NOT NULL,
                    created_at INTEGER NOT NULL
                );

-- Index for queries filtering by user
                CREATE INDEX IF NOT EXISTS idx_messages_user_id ON messages(user_id);

-- Index for time-based queries (recent messages)
                CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);

-- Composite index for user + time queries
                CREATE INDEX IF NOT EXISTS idx_messages_user_time ON messages(user_id, created_at);
            `);
        });
    }

// This query benefits from idx_messages_user_time
    async getUserMessages(userId: string, since: number) {
        return this.ctx.storage.sql
            .exec(
                "SELECT * FROM messages WHERE user_id = ? AND created_at > ? ORDER BY created_at",
                userId,
                since
            )
            .toArray();
    }
}

Understand how input and output gates work

While Durable Objects are single-threaded, JavaScript's async/await can allow multiple requests to interleave execution while a request waits for the result of an asynchronous operation. Cloudflare's runtime uses input gates and output gates to prevent data races and ensure correctness by default.

Input gates block new events (incoming requests, fetch responses) while synchronous JavaScript execution is in progress. Awaiting async operations like fetch() or KV storage methods opens the input gate, allowing other requests to interleave. However, storage operations provide special protection: