Configure the binding
To add a Durable Object class, declare a binding and a migration for the class in thewrangler.json file:
wrangler.json
- Each class needs a
new_sqlite_classesmigration before its first deploy. - To add another class, append an entry with a tag that you haven’t used before, for example
{ "tag": "v2", "new_sqlite_classes": ["Presence"] }. Don’t change or remove earlier entries. - A binding has only the
nameandclass_namekeys. Thenamevalue is theenv.NAMEproperty in your code. - You must export each class that the configuration names from the
index.jsfile. - Deploy refuses the
script_name,new_classes,renamed_classes,deleted_classes, andtransferred_classeskeys.
Address an object
You reach an object through a stub. The following example shows each way to get a stub:- A stub creates nothing. The object starts at the first call.
- Inside the object,
ctx.id.namereturns the object’s name. - There is no API to list objects. If you need an index of objects, keep one in D1.
- One object accepts a maximum of 64 concurrent requests, and then returns
503. Don’t send all traffic to one global object. Use one object for each room, user, or document. To spread hot data, shard it across several objects (for example,counter-${hash % 16}), or use D1.
Write the class
The following class stores chat messages in SQLite and exposes two RPC methods:index.js
- Extend the
DurableObjectclass fromcloudflare:workers. RPC requires it, unless you set thejs_rpccompatibility flag. - Use RPC methods for calls. Implement the
fetchmethod only for WebSockets or a full HTTP proxy. - RPC arguments and return values must be structured-cloneable. Class instances with methods aren’t.
- The constructor runs at each activation. Keep it fast and idempotent.
- Treat fields on
thisas a cache only. celld evicts idle objects and moves objects between nodes, so keep all state inctx.storage. - You don’t need
ctx.waitUntilin an object. Pending I/O stays alive after the handler returns.
Writes and responses
You don’t need toawait a write before you return a response. The output gate holds the response until all writes are durable.
The output gate also holds each outbound fetch until the writes before it are durable. Before an external side effect that depends on a write, also call await this.ctx.storage.sync(). For example, if you record a “running” mark and then call a payment API, sync the mark first. The call makes the order clear in the code, and it throws before the side effect if the write can’t become durable.
Store data
Each object has one SQLite database. You can use it through SQL or through a key-value API, and both share the same database.Query with SQL
Thectx.storage.sql.exec(sql, ...bindings) method is synchronous and returns a cursor. Read the cursor with .toArray(), .one(), .next(), .raw(), or a for...of loop.
Read each write cursor to its end. An open INSERT ... RETURNING cursor holds uncommitted writes, and celld then refuses the response.
Store bytes in BLOB columns. TEXT values must be valid UTF-8.
To get the size of the database in bytes, read the ctx.storage.sql.databaseSize property.
Use the key-value API
The key-value API stores values in the same database. The following methods are asynchronous:ctx.storage.getctx.storage.putctx.storage.deletectx.storage.list({ prefix, limit, reverse })ctx.storage.deleteAll()
ctx.storage.kv.get, ctx.storage.kv.put, ctx.storage.kv.delete, and ctx.storage.kv.list.
Make writes atomic
To make several writes atomic, wrap them inctx.storage.transactionSync(() => { ... }). If the callback throws, then celld rolls back its writes. Transactions can nest.
Don’t write BEGIN or COMMIT statements.
Change the schema inside an object
Each object has its own database, so themigrations/ directory doesn’t apply to Durable Objects. Instead, keep a schema version in the object and apply pending changes in the constructor:
index.js
MIGRATIONS array, and make each change additive. An object runs the updated code at its next activation.
To stop other events while setup runs, use ctx.blockConcurrencyWhile(async () => { ... }). The callback has a 30-second limit. Use it only for setup.
Set an alarm
Each object has one alarm. Use it for timers that belong to one object and for background work in steps.setAlarm(ms)replaces the current alarm.getAlarm()returns the scheduled time, ornull.deleteAlarm()cancels the alarm.deleteAll()also deletes the alarm.- The alarm is durable before the object sends its response.
- If the
alarmhandler throws, then celld runs it again with backoff. TheretryCountandisRetryarguments report the retry. Make thealarmhandler idempotent.
alarm handler, do all work that is due, and then set the alarm to the next due time.
Handle WebSockets with hibernation
A Durable Object can accept WebSockets and hibernate while they’re idle. The Worker checks for an upgrade request and forwards it to the object:index.js
Room class from Write the class:
index.js
- A clean close calls
webSocketClose. An unclean close, such as a dropped connection, callswebSocketErrorinstead. Do your cleanup in both handlers, for example for presence. - Accept sockets with
this.ctx.acceptWebSocket(server). Don’t callserver.accept(), because that socket doesn’t hibernate and closes when the event ends. - Get the open sockets with
this.ctx.getWebSockets(tag). Thetagargument is optional. Don’t keep sockets in aSet. - Keep small per-socket state with the
serializeAttachmentanddeserializeAttachmentmethods. - To answer heartbeats without waking the object, call
this.ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair("ping", "pong")). - Message handlers start in arrival order, but they don’t wait for each other. If order matters, write to SQL before the first
await. - Keep frames small. Each isolate has a 1 MiB input queue.
1012 when the object moves to a different node, for example during a deploy. Make the client reconnect with backoff and load the history again.
Call an object from the Worker
When you call RPC methods from the Worker, handle these cases:- An error thrown in an RPC method rejects the promise in the caller. Catch the error and return a
4xxor5xxresponse. - celld retries an RPC call only if the method didn’t start. Make methods idempotent, or accept a request ID from the client.
- You can’t send RPC stubs between isolates. Return data instead.
Limits
The following table lists the limits for Durable Objects on celld:Differences from Cloudflare
celld differs from Cloudflare Durable Objects in the following ways:- Durable Object bindings to other scripts aren’t available. Deploy refuses the
script_namekey. - You can’t rename, delete, or transfer classes. Deploy refuses
new_classes,renamed_classes,deleted_classes, andtransferred_classes. Usenew_sqlite_classesfor every class. - Jurisdiction APIs throw.
- One object accepts a maximum of 64 concurrent requests, and then returns
503. - An open write cursor makes celld refuse the response.
- A hibernating WebSocket closes with code
1012when the object moves to a different node. - RPC stubs can’t cross isolates, and celld retries an RPC call only if the method didn’t start.