Cloudflare Durable Objects
State adapter for Chat SDK backed by a SQLite Durable Object. Persistent subscriptions, distributed locking, queues, lists, and caching from inside Cloudflare Workers — no Redis, no database.
Install
pnpm add chat chat-state-cloudflare-doQuick start
import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import { ChatStateDO, createCloudflareState } from "chat-state-cloudflare-do";
// Re-export the Durable Object class so Cloudflare can find it.
export { ChatStateDO };
export default {
async fetch(request: Request, env: Env) {
const bot = new Chat({
userName: "my-bot",
adapters: { slack: createSlackAdapter() },
state: createCloudflareState({ namespace: env.CHAT_STATE }),
});
return bot.webhooks.slack(request);
},
};Wrangler configuration
Add the Durable Object binding and migration to your wrangler.jsonc (recommended) or wrangler.toml:
{
"durable_objects": {
"bindings": [{ "name": "CHAT_STATE", "class_name": "ChatStateDO" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatStateDO"] }]
}[durable_objects]
bindings = [
{ name = "CHAT_STATE", class_name = "ChatStateDO" }
]
[[migrations]]
tag = "v1"
new_sqlite_classes = ["ChatStateDO"]Environment type
import type { ChatStateDO } from "chat-state-cloudflare-do";
interface Env {
CHAT_STATE: DurableObjectNamespace<ChatStateDO>;
}Configuration
Prop
Type
Sharding
A single Durable Object handles roughly 500–1000 requests per second. For high-traffic bots, use shardKey to distribute load across multiple DO instances:
const state = createCloudflareState({
namespace: env.CHAT_STATE,
shardKey: (threadId) => threadId.split(":")[0], // one DO per platform
});Locks, force-release, and queue operations are per-thread, so sharding by any prefix of the thread ID is safe. Cache and list operations (get / set / delete, appendToList / getList) always route to the default shard since their keys are not thread-scoped.
| Strategy | shardKey | DOs created |
|---|---|---|
| No sharding (default) | – | 1 |
| Per platform | (id) => id.split(":")[0] | 1 per platform |
| Per channel | (id) => id.split(":").slice(0, 2).join(":") | 1 per channel |
Architecture
The adapter uses a single Durable Object class (ChatStateDO) with five SQLite tables:
subscriptions— thread IDs the bot is subscribed tolocks— distributed locks with token-based ownership and TTLcache— key-value pairs with optional TTLqueue— thread-scoped FIFO queue entries with TTL for concurrency strategieslists— ordered list entries for persistent message history
All operations are single-threaded within a DO instance, which gives you distributed locking via DO atomicity rather than Lua scripts. Expired entries are cleaned up automatically via the Alarms API.
Each method call creates a fresh DO stub. Stubs are cheap (just a JS object) and the Cloudflare docs recommend creating new stubs rather than reusing them after errors.
Capabilities
- Persistent subscriptions across deployments
- Distributed locking via single-threaded DO atomicity
- Lock force-release for Chat SDK lock conflict handling
- Queue/debounce concurrency primitives
- List-backed persistent message history
- Key-value caching with TTL
- Automatic TTL cleanup via Alarms
- Optional sharding for high-traffic bots
- Location hints for latency optimization
- Zero external dependencies (no Redis, no database)
Production recommendations
- Use Smart Placement to co-locate your Worker with the DO.
- Monitor DO metrics in the Cloudflare dashboard.
- Enable sharding if you expect more than ~500 req/s to a single DO instance.
- Use
locationHintto place the DO near your primary user base.
Feature support
Capabilities
| Feature | Supported |
|---|---|
| Persistence | |
| Multi-instance | |
| Subscriptions | |
| Distributed locking | |
| Key-value caching | |
| Lists | |
| Queues | |
| Automatic reconnect | |
| Cluster support | Sharding |
| Sentinel support | |
| Key prefix namespacing | shardKey |