Durable Objects
- Demos and architectures
- Getting started
- Cloudflare Durable Objects
- Release notes
- Videos
- Alarms
- Durable Object Base Class
- Durable Object ID
- Workers Binding API
- KV-backed Durable Object Storage (Legacy)
- Durable Object Namespace
- SQLite-backed Durable Object Storage
- Durable Object State
- Durable Object Stub
- WebGPU
- Access Durable Objects Storage
- Invoke methods
- Error handling
- Best practices
- Rules of Durable Objects
- Use WebSockets
- Lifecycle of a Durable Object
- Concepts
- What are Durable Objects?
- Durable Objects code updates
- Deployments
- Use the Alarms API
- Build a counter
- Durable Object in-memory state
- Durable Object Time To Live
- Examples
- Use ReadableStream with Durable Object and Workers
- Use RpcTarget class to handle Durable Object metadata
- Testing Durable Objects
- Use Workers KV from Durable Objects
- Build a WebSocket server with WebSocket Hibernation
- Build a WebSocket server
- Data Studio
- Observability
- Metrics and analytics
- Troubleshooting
- Platform
- Known issues
- Limits
- Pricing
- Data location
- Data security
- Durable Object class migrations (legacy)
- Durable Object class exports
- Environments
- FAQs
- Glossary
- In-memory state in a Durable Object
- Reference
- Build a seat booking app with SQLite in Durable Objects
- Tutorials
Demos and architectures
import { GlossaryTooltip, ResourcesBySelector } from "~/components";
Learn how you can use a Durable Object within your existing application and architecture.
Reference architectures
Explore the following reference architectures that use Durable Objects:
<ResourcesBySelector types={[ "reference-architecture", "design-guide", "reference-architecture-diagram", ]} products={["durable-objects"]} />
Getting started
import { Render, TabItem, Tabs, PackageManagers, WranglerConfig, TypeScriptExample, DashButton } from "~/components";
This guide will instruct you through:
- Writing a JavaScript class that defines a Durable Object.
- Using Durable Objects SQL API to query a Durable Object's private, embedded SQLite database.
- Instantiating and communicating with a Durable Object from another Worker.
- Deploying a Durable Object and a Worker that communicates with a Durable Object.
If you wish to learn more about Durable Objects, refer to What are Durable Objects?.
Quick start
If you want to skip the steps and get started quickly, click on the button below.
This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. Use this option if you are familiar with Cloudflare Workers, and wish to skip the step-by-step guidance.
You may wish to manually follow the steps if you are new to Cloudflare Workers.
Prerequisites
1. Create a Worker project
You will access your Durable Object from a Worker. Your Worker application is an interface to interact with your Durable Object.
To create a Worker project, run:
<PackageManagers type="create" pkg="cloudflare@latest" args={"durable-object-starter"} />
Running create cloudflare@latest will install Wrangler, the Workers CLI. You will use Wrangler to test and deploy your project.
<Render file="c3-post-run-steps-hello-world" product="workers" params={{ type: "Worker + Durable Objects", lang: "TypeScript", }} />
This will create a new directory, which will include either a src/index.js or src/index.ts file to write your code and a wrangler.jsonc configuration file.
Move into your new directory:
cd durable-object-starter
:::note[Adding a Durable Object to an existing Worker]
To add a Durable Object to an existing Worker, you need to:
-
Modify the code of the existing Worker to include the following:
export class MyDurableObject extends DurableObject<Env> { constructor(ctx: DurableObjectState, env: Env) { // Required, as we're extending the base class. super(ctx, env) } {/* Define your Durable Object methods here */} } export default { async fetch(request, env, ctx): Promise<Response> { const stub = env.MY_DURABLE_OBJECT.getByName(new URL(request.url).pathname); {/* Access your Durable Object methods here */} }, } satisfies ExportedHandler<Env>; -
Update the Wrangler configuration file of your existing Worker to bind the Durable Object to the Worker.
:::
2. Write a Durable Object class using SQL API
Before you create and access a Durable Object, its behavior must be defined by an ordinary exported JavaScript class.
:::note If you do not use JavaScript or TypeScript, you will need a shim to translate your class definition to a JavaScript class. :::
Your MyDurableObject class will have a constructor with two parameters. The first parameter, ctx, passed to the class constructor contains state specific to the Durable Object, including methods for accessing storage. The second parameter, env, contains any bindings you have associated with the Worker when you uploaded it.
from workers import DurableObject
class MyDurableObject(DurableObject):
def __init__(self, ctx, env):
super().__init__(ctx, env)
Workers communicate with a Durable Object using remote-procedure call. Public methods on a Durable Object class are exposed as RPC methods to be called by another Worker.
Your file should now look like:
```ts export class MyDurableObject extends DurableObject { constructor(ctx: DurableObjectState, env: Env) { // Required, as we're extending the base class. super(ctx, env) }async sayHello(): Promise<string> {
let result = this.ctx.storage.sql
.exec<{ greeting: string }>("SELECT 'Hello, World!' as greeting")
.one();
return result.greeting;
}
}
</TypeScriptExample>
<TabItem label="Python" icon="seti:python">
```python
from workers import DurableObject
class MyDurableObject(DurableObject):
async def say_hello(self):
result = self.ctx.storage.sql.exec(
"SELECT 'Hello, World!' as greeting"
).one()
return result.greeting
In the code above, you have:
- Defined a RPC method,
sayHello(), that can be called by a Worker to communicate with a Durable Object. - Accessed a Durable Object's attached storage, which is a private SQLite database only accessible to the object, using SQL API methods (
sql.exec()) available onctx.storage. - Returned an object representing the single row query result using
one(), which checks that the query result has exactly one row. - Return the
greetingcolumn from the row object result.
3. Instantiate and communicate with a Durable Object
:::note
Durable Objects do not receive requests directly from the Internet. Durable Objects receive requests from Workers or other Durable Objects. This is achieved by configuring a binding in the calling Worker for each Durable Object class that you would like it to be able to talk to. These bindings must be configured at upload time. Methods exposed by the binding can be used to communicate with particular Durable Objects. :::
A Worker is used to access Durable Objects.
To communicate with a Durable Object, the Worker's fetch handler should look like the following:
```ts export default { async fetch(request, env, ctx): Promise { const stub = env.MY_DURABLE_OBJECT.getByName(new URL(request.url).pathname); const greeting = await stub.sayHello();
return new Response(greeting);
},
} satisfies ExportedHandler;
</TypeScriptExample>
<TabItem label="Python" icon="seti:python">
```python
from workers import handler, Response, WorkerEntrypoint
from urllib.parse import urlparse
class Default(WorkerEntrypoint):
async def fetch(request):
url = urlparse(request.url)
stub = self.env.MY_DURABLE_OBJECT.getByName(url.path)
greeting = await stub.say_hello()
return Response(greeting)
In the code above, you have:
- Exported your Worker's main event handlers, such as the
fetch()handler for receiving HTTP requests. - Passed
envinto thefetch()handler. Bindings are delivered as a property of the environment object passed as the second parameter when an event handler or class constructor is invoked. - Constructed a stub for a Durable Object instance based on the provided name. A stub is a client object used to send messages to the Durable Object.
- Called a Durable Object by invoking a RPC method,
sayHello(), on the Durable Object, which returns aHello, World!string greeting. - Received an HTTP response back to the client by constructing a HTTP Response with
return new Response().
Refer to Access a Durable Object from a Worker to learn more about communicating with a Durable Object.
4. Configure Durable Object bindings
Bindings allow your Workers to interact with resources on the Cloudflare developer platform. The Durable Object bindings in your Worker project's Wrangler configuration file will include a binding name (for this guide, use MY_DURABLE_OBJECT) and the class name (MyDurableObject).
{
"durable_objects": {
"bindings": [
{
"name": "MY_DURABLE_OBJECT",
"class_name": "MyDurableObject"
}
]
}
}
The bindings section contains the following fields:
name- Required. The binding name to use within your Worker.class_name- Required. The class name you wish to bind to.script_name- Optional. Defaults to the current environment's Worker code.
5. Configure Durable Object class with SQLite storage backend
You declare each Durable Object class your Worker exports in the exports field of your Wrangler configuration file. Cloudflare uses this declaration to provision a namespace for the class the first time you deploy, and to manage its lifecycle on later deploys (rename, delete, transfer).
The minimal exports block to register a new Durable Object class with SQLite storage looks like:
{
"exports": {
"MyDurableObject": {
"type": "durable-object",
"storage": "sqlite"
}
}
}
Refer to Durable Object class exports to learn more about declaring and managing Durable Object classes. If you have an existing Worker on the legacy migrations array, refer to Durable Object class migrations (legacy).
6. Develop a Durable Object Worker locally
To test your Durable Object locally, run wrangler dev:
npx wrangler dev
In your console, you should see aHello world string returned by the Durable Object.
7. Deploy your Durable Object Worker
To deploy your Durable Object Worker:
npx wrangler deploy
Once deployed, you should be able to see your newly created Durable Object Worker on the Cloudflare dashboard.
Preview your Durable Object Worker at <YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev.
Summary and final code
Your final code should look like this:
```ts title="index.ts" import { DurableObject } from "cloudflare:workers"; export class MyDurableObject extends DurableObject { constructor(ctx: DurableObjectState, env: Env) { // Required, as we are extending the base class. super(ctx, env) }async sayHello():Promise<string> {
let result = this.ctx.storage.sql
.exec("SELECT 'Hello, World!' as greeting")
.one();
return result.greeting;
}
} export default { async fetch(request, env, ctx): Promise { const stub = env.MY_DURABLE_OBJECT.getByName(new URL(request.url).pathname);
const greeting = await stub.sayHello();
return new Response(greeting);
},
} satisfies ExportedHandler;
</TypeScriptExample>
<TabItem label="Python" icon="seti:python">
```python
from workers import DurableObject, handler, Response
from urllib.parse import urlparse
class MyDurableObject(DurableObject):
async def say_hello(self):
result = self.ctx.storage.sql.exec(
"SELECT 'Hello, World!' as greeting"
).one()
return result.greeting
class Default(WorkerEntrypoint):
async def fetch(self, request):
url = urlparse(request.url)
stub = self.env.MY_DURABLE_OBJECT.getByName(url.path)
greeting = await stub.say_hello()
return Response(greeting)
By finishing this tutorial, you have:
- Successfully created a Durable Object
- Called the Durable Object by invoking a RPC method
- Deployed the Durable Object globally
Related resources
- Create Durable Object stubs
- Access Durable Objects Storage
- Miniflare - Helpful tools for mocking and testing your Durable Objects.
Cloudflare Durable Objects
import { Render, CardGrid, Description, Feature, LinkTitleCard, Plan, RelatedProduct, LinkButton, } from "~/components";
Create AI agents, collaborative applications, real-time interactions like chat, and more without needing to coordinate state, have separate storage, or manage infrastructure.Durable Objects provide a building block for stateful applications and distributed systems.
Use Durable Objects to build applications that need coordination among multiple clients, like collaborative editing tools, interactive chat, multiplayer games, live notifications, and deep distributed systems, without requiring you to build serialization and coordination primitives on your own.
Get started
:::note SQLite-backed Durable Objects are now available on the Workers Free plan with these limits.
SQLite storage and corresponding Storage API methods like sql.exec have moved from beta to general availability. New Durable Object classes should use wrangler configuration for SQLite storage.
:::
What are Durable Objects?
For more information, refer to the full What are Durable Objects? page.
Features
Learn how Durable Objects coordinate connections among multiple clients or events.
Learn how Durable Objects provide transactional, strongly consistent, and serializable storage.
Learn how WebSocket Hibernation allows you to manage the connections of multiple clients at scale.
Learn how to use alarms to trigger a Durable Object and perform compute in the future at customizable intervals.
Related products
Cloudflare Workers provides a serverless execution environment that allows you to create new applications or augment existing ones without configuring or maintaining infrastructure.
D1 is Cloudflare's SQL-based native serverless database. Create a database by importing data or defining your tables and writing your queries within a Worker or through the API.
Cloudflare R2 Storage allows developers to store large amounts of unstructured data without the costly egress bandwidth fees associated with typical cloud storage services.
More resources
<LinkTitleCard title="Limits" href="/durable-objects/platform/limits/" icon="document"
Learn about Durable Objects limits.
<LinkTitleCard title="Pricing" href="/durable-objects/platform/pricing/" icon="pen"
Learn about Durable Objects pricing.
<LinkTitleCard title="Storage options" href="/workers/platform/storage-options/" icon="document"
Learn more about storage and database options you can build with Workers.
<LinkTitleCard title="Developer Discord" href="https://discord.cloudflare.com" icon="discord"
Connect with the Workers community on Discord to ask questions, show what you
are building, and discuss the platform with other developers.
<LinkTitleCard title="@CloudflareDev" href="https://x.com/cloudflaredev" icon="x.com"
Follow @CloudflareDev on Twitter to learn about product announcements, and
what is new in Cloudflare Developer Platform.
Release notes
import { ProductReleaseNotes } from "~/components";
{/* */}
Videos
import { CardGrid, LinkCard } from "~/components";
Alarms
import { Type, GlossaryTooltip, Tabs, TabItem } from "~/components";
Background
Durable Objects alarms allow you to schedule the Durable Object to be woken up at a time in the future. When the alarm's scheduled time comes, the alarm() handler method will be called. Alarms are modified using the Storage API, and alarm operations follow the same rules as other storage operations.
Notably:
- Each Durable Object is able to schedule a single alarm at a time by calling
setAlarm(). - Alarms have guaranteed at-least-once execution and are retried automatically when the
alarm()handler throws. - Retries are performed using exponential backoff starting at a 2 second delay from the first failure with up to 6 retries allowed.
:::note[How are alarms different from Cron Triggers?]
Alarms are more fine grained than Cron Triggers. A Worker can have up to three Cron Triggers configured at once, but it can have an unlimited amount of Durable Objects, each of which can have an alarm set.
Alarms are directly scheduled from within your Durable Object. Cron Triggers, on the other hand, are not programmatic. Cron Triggers execute based on their schedules, which have to be configured through the Cloudflare dashboard or API.
:::
Alarms can be used to build distributed primitives, like queues or batching of work atop Durable Objects. Alarms also provide a mechanism to guarantee that operations within a Durable Object will complete without relying on incoming requests to keep the Durable Object alive. For a complete example, refer to Use the Alarms API.
Scheduling multiple events with a single alarm
Although each Durable Object can only have one alarm set at a time, you can manage many scheduled and recurring events by storing your event schedule in storage and having the alarm() handler process due events, then reschedule itself for the next one.
import { DurableObject } from "cloudflare:workers";
export class AgentServer extends DurableObject {
// Schedule a one-time or recurring event
async scheduleEvent(id, runAt, repeatMs = null) {
await this.ctx.storage.put(`event:${id}`, { id, runAt, repeatMs });
const currentAlarm = await this.ctx.storage.getAlarm();
if (!currentAlarm || runAt < currentAlarm) {
await this.ctx.storage.setAlarm(runAt);
}
}
async alarm() {
const now = Date.now();
const events = await this.ctx.storage.list({ prefix: "event:" });
let nextAlarm = null;
for (const [key, event] of events) {
if (event.runAt <= now) {
await this.processEvent(event);
if (event.repeatMs) {
event.runAt = now + event.repeatMs;
await this.ctx.storage.put(key, event);
} else {
await this.ctx.storage.delete(key);
}
}
// Track the next event time
if (event.runAt > now && (!nextAlarm || event.runAt < nextAlarm)) {
nextAlarm = event.runAt;
}
}
if (nextAlarm) await this.ctx.storage.setAlarm(nextAlarm);
}
async processEvent(event) {
// Your event handling logic here
}
}
Storage methods
getAlarm
-
getAlarm():-
If there is an alarm set, then return the currently set alarm time as the number of milliseconds elapsed since the UNIX epoch. Otherwise, return
null. -
If
getAlarmis called while analarmis already running, it returnsnullunlesssetAlarmhas also been called since the alarm handler started running.
-
setAlarm
-
setAlarm(scheduledTimeMs ):- Set the time for the alarm to run. Specify the time as the number of milliseconds elapsed since the UNIX epoch.
- If you call
setAlarmwhen there is already one scheduled, it will override the existing alarm.
:::caution[Calling setAlarm inside the constructor]
If you wish to call setAlarm inside the constructor of a Durable Object, ensure that you are first checking whether an alarm has already been set.
This is due to the fact that, if the Durable Object wakes up after being inactive, the constructor is invoked before the alarm handler. Therefore, if the constructor calls setAlarm, it could interfere with the next alarm which has already been set.
:::
deleteAlarm
-
deleteAlarm():-
Unset the alarm if there is a currently set alarm.
-
Calling
deleteAlarm()inside thealarm()handler may prevent retries on a best-effort basis, but is not guaranteed.
-
Handler methods
alarm
-
alarm(alarmInfo ):-
Called by the system when a scheduled alarm time is reached.
-
The optional parameter
alarmInfoobject has two properties:retryCount: The number of times this alarm event has been retried.isRetry: A boolean value to indicate if the alarm has been retried. This value istrueif this alarm event is a retry.
-
Only one instance of
alarm()will ever run at a given time per Durable Object instance. -
The
alarm()handler has guaranteed at-least-once execution and will be retried upon failure using exponential backoff, starting at 2 second delays for up to 6 retries. This only applies to the most recentsetAlarm()call. Retries will be performed if the method fails with an uncaught exception. -
This method can be
async.
-
:::note[Catching exceptions in alarm handlers]
Because alarms are only retried up to 6 times on error, it's recommended to catch any exceptions inside your alarm() handler and schedule a new alarm before returning if you want to make sure your alarm handler will be retried indefinitely. Otherwise, a sufficiently long outage in a downstream service that you depend on or a bug in your code that goes unfixed for hours can exhaust the limited number of retries, causing the alarm to not be re-run in the future until the next time you call setAlarm.
:::
Example
This example shows how to both set alarms with the setAlarm(timestamp) method and handle alarms with the alarm() handler within your Durable Object.
- The
alarm()handler will be called once every time an alarm fires. - If an unexpected error terminates the Durable Object, the
alarm()handler may be re-instantiated on another machine. - Following a short delay, the
alarm()handler will run from the beginning on the other machine.
import { DurableObject } from "cloudflare:workers";
export default {
async fetch(request, env) {
return await env.ALARM_EXAMPLE.getByName("foo").fetch(request);
},
};
const SECONDS = 1000;
export class AlarmExample extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
this.storage = ctx.storage;
}
async fetch(request) {
// If there is no alarm currently set, set one for 10 seconds from now
let currentAlarm = await this.storage.getAlarm();
if (currentAlarm == null) {
this.storage.setAlarm(Date.now() + 10 * SECONDS);
}
}
async alarm() {
// The alarm handler will be invoked whenever an alarm fires.
// You can use this to do work, read from the Storage API, make HTTP calls
// and set future alarms to run using this.storage.setAlarm() from within this handler.
}
}
import time
from workers import DurableObject, WorkerEntrypoint
class Default(WorkerEntrypoint):
async def fetch(self, request):
return await self.env.ALARM_EXAMPLE.getByName("foo").fetch(request)
SECONDS = 1000
class AlarmExample(DurableObject):
def __init__(self, ctx, env):
super().__init__(ctx, env)
self.storage = ctx.storage
async def fetch(self, request):
# If there is no alarm currently set, set one for 10 seconds from now
current_alarm = await self.storage.getAlarm()
if current_alarm is None:
self.storage.setAlarm(int(time.time() * 1000) + 10 * SECONDS)
async def alarm(self):
# The alarm handler will be invoked whenever an alarm fires.
# You can use this to do work, read from the Storage API, make HTTP calls
# and set future alarms to run using self.storage.setAlarm() from within this handler.
pass
The following example shows how to use the alarmInfo property to identify if the alarm event has been attempted before.
class MyDurableObject extends DurableObject {
async alarm(alarmInfo) {
if (alarmInfo?.retryCount != 0) {
console.log(
"This alarm event has been attempted ${alarmInfo?.retryCount} times before.",
);
}
}
}
class MyDurableObject(DurableObject):
async def alarm(self, alarm_info):
if alarm_info and alarm_info.get('retryCount', 0) != 0:
print(f"This alarm event has been attempted {alarm_info.get('retryCount')} times before.")
Related resources
- Understand how to use the Alarms API in an end-to-end example.
- Read the Durable Objects alarms announcement blog post.
- Review the Storage API documentation for Durable Objects.
Durable Object Base Class
import { Render, Tabs, TabItem, GlossaryTooltip, Type, MetaInfo, TypeScriptExample, } from "~/components";
The DurableObject base class is an abstract class which all Durable Objects inherit from. This base class provides a set of optional methods, frequently referred to as handler methods, which can respond to events, for example a webSocketMessage when using the WebSocket Hibernation API. To provide a concrete example, here is a Durable Object MyDurableObject which extends DurableObject and implements the fetch handler to return "Hello, World!" to the calling Worker.
async fetch(request: Request) {
return new Response("Hello, World!");
}
}
</TypeScriptExample>
<TabItem label="Python" icon="seti:python">
```python
from workers import DurableObject, Response
class MyDurableObject(DurableObject):
def __init__(self, ctx, env):
super().__init__(ctx, env)
async def fetch(self, request):
return Response("Hello, World!")
Methods
fetch
fetch(request ): | - Takes an HTTP Request and returns an HTTP Response. This method allows the Durable Object to emulate an HTTP server where a Worker with a binding to that object is the client. - This method can beasync.- Durable Objects support RPC calls as of compatibility date 2024-04-03. RPC methods are preferred over
fetch()when your application does not follow HTTP request/response flow.
- Durable Objects support RPC calls as of compatibility date 2024-04-03. RPC methods are preferred over
Parameters
request- the incoming HTTP request object.
Return values
- A or .
Example
```ts export class MyDurableObject extends DurableObject { async fetch(request: Request): Promise { const url = new URL(request.url); if (url.pathname === "/hello") { return new Response("Hello, World!"); } return new Response("Not found", { status: 404 }); } } ```from workers import DurableObject, Response
from urllib.parse import urlparse
class MyDurableObject(DurableObject):
async def fetch(self, request):
path = urlparse(request.url).path
if path == "/hello":
return Response("Hello, World!")
return Response("Not found", status=404)
alarm
alarm(alarmInfo? ): |- Called by the system when a scheduled alarm time is reached.
- The
alarm()handler has guaranteed at-least-once execution and will be retried upon failure using exponential backoff, starting at two second delays for up to six retries. Retries will be performed if the method fails with an uncaught exception. - This method can be
async. - Refer to Alarms for more information.
Parameters
alarmInfo(optional) - an object containing retry information:retryCount- the number of times this alarm event has been retried.isRetry-trueif this alarm event is a retry,falseotherwise.
Return values
- None.
Example
```ts export class MyDurableObject extends DurableObject { async alarm(alarmInfo?: AlarmInvocationInfo): Promise { if (alarmInfo?.isRetry) { console.log(`Alarm retry attempt ${alarmInfo.retryCount}`); } await this.processScheduledTask(); } } ```from workers import DurableObject
class MyDurableObject(DurableObject):
async def alarm(self, alarm_info=None):
if alarm_info and alarm_info.isRetry:
print(f"Alarm retry attempt {alarm_info.retryCount}")
await self.process_scheduled_task()
webSocketMessage
webSocketMessage(ws , message ): | - Called by the system when an accepted WebSocket receives a message. - This method is not called for WebSocket control frames. The system will respond to an incoming WebSocket protocol ping automatically without interrupting hibernation.- This method can be
async.
- This method can be
Parameters
ws- the WebSocket that received the message. Use this reference to send responses or access serialized attachments.message- the message data. Text messages arrive asstring, binary messages asArrayBuffer.
Return values
- None.
Example
```ts export class MyDurableObject extends DurableObject { async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) { if (typeof message === "string") { ws.send(`Received: ${message}`); } else { ws.send(`Received ${message.byteLength} bytes`); } } } ```from workers import DurableObject
class MyDurableObject(DurableObject):
async def webSocketMessage(self, ws, message):
if isinstance(message, str):
ws.send(f"Received: {message}")
else:
ws.send(f"Received {len(message)} bytes")
webSocketClose
webSocketClose(ws , code , reason , wasClean ): | - Called by the system when a WebSocket connection is closed.- With the
web_socket_auto_reply_to_closecompatibility flag (enabled by default on compatibility dates on or after2026-04-07), the runtime automatically sends a reciprocal Close frame and transitionsreadyStatetoCLOSEDbefore this handler is called. You do not need to callws.close()— but doing so is safe (the call is silently ignored). - On older compatibility dates (before
2026-04-07), you must callws.close(code, reason)inside this handler to complete the WebSocket close handshake. Failing to reciprocate the close will result in1006errors on the client, representing an abnormal closure per the WebSocket specification. - This method can be
async.
- With the
Parameters
ws- the WebSocket that was closed.code- the WebSocket close code sent by the peer (e.g.,1000for normal closure,1001for going away).reason- a string indicating why the connection was closed. May be empty.wasClean-trueif the connection closed cleanly with a proper closing handshake,falseotherwise.
Return values
- None.
Example
```ts export class MyDurableObject extends DurableObject { async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) { // With web_socket_auto_reply_to_close (compat date >= 2026-04-07), // the runtime has already completed the close handshake. // On older compat dates, call ws.close(code, reason) here. ws.close(code, reason); console.log(`WebSocket closed: code=${code}, reason=${reason}`); } } ```from workers import DurableObject
class MyDurableObject(DurableObject):
async def webSocketClose(self, ws, code, reason, was_clean):
ws.close(code, reason)
print(f"WebSocket closed: code={code}, reason={reason}")
webSocketError
webSocketError(ws , error ): | - Called by the system when a non-disconnection error occurs on a WebSocket connection. - This method can beasync.
Parameters
ws- the WebSocket that encountered an error.error- the error that occurred. May be anErrorobject or another type depending on the error source.
Return values
- None.
Example
```ts export class MyDurableObject extends DurableObject { async webSocketError(ws: WebSocket, error: unknown) { const message = error instanceof Error ? error.message : String(error); console.error(`WebSocket error: ${message}`); } } ```from workers import DurableObject
class MyDurableObject(DurableObject):
async def webSocketError(self, ws, error):
print(f"WebSocket error: {error}")
Properties
ctx
ctx is a readonly property of type DurableObjectState providing access to storage, WebSocket management, and other instance-specific functionality.
env
env contains the environment bindings available to this Durable Object, as defined in your Wrangler configuration.
Related resources
- Use WebSockets for WebSocket handler best practices.
- Alarms API for scheduling future work.
- RPC methods for type-safe method calls.
Durable Object ID
import { Render, Tabs, TabItem, GlossaryTooltip } from "~/components";
Description
A Durable Object ID is a 64-digit hexadecimal number used to identify a Durable Object. Not all 64-digit hex numbers are valid IDs. Durable Object IDs are constructed indirectly via the DurableObjectNamespace interface.
The DurableObjectId interface refers to a new or existing Durable Object. This interface is most frequently used by DurableObjectNamespace::get to obtain a DurableObjectStub for submitting requests to a Durable Object. Note that creating an ID for a Durable Object does not create the Durable Object. The Durable Object is created lazily after creating a stub from a DurableObjectId. This ensures that objects are not constructed until they are actually accessed.
:::note[Logging]
If you are experiencing an issue with a particular Durable Object, you may wish to log the DurableObjectId from your Worker and include it in your Cloudflare support request.
:::
Methods
toString
toString converts a DurableObjectId to a 64 digit hex string. This string is useful for logging purposes or storing the DurableObjectId elsewhere, for example, in a session cookie. This string can be used to reconstruct a DurableObjectId via DurableObjectNamespace::idFromString.
Parameters
- None.
Return values
- A 64 digit hex string.
equals
equals is used to compare equality between two instances of DurableObjectId.
const id1 = env.MY_DURABLE_OBJECT.newUniqueId();
const id2 = env.MY_DURABLE_OBJECT.newUniqueId();
console.assert(!id1.equals(id2), "Different unique ids should never be equal.");
id1 = env.MY_DURABLE_OBJECT.newUniqueId()
id2 = env.MY_DURABLE_OBJECT.newUniqueId()
assert not id1.equals(id2), "Different unique ids should never be equal."
Parameters
- A required
DurableObjectIdto compare against.
Return values
- A boolean. True if equal and false otherwise.
Properties
name
name is an optional property of a DurableObjectId, which returns the name that was used to create the DurableObjectId via DurableObjectNamespace::idFromName. This value is undefined if the DurableObjectId was constructed using DurableObjectNamespace::newUniqueId.
The name property is also available on ctx.id inside the Durable Object when the caller uses idFromName() or getByName(). ctx.id.name will be undefined in the following cases:
- The caller accesses the Durable Object using
idFromString(), even if the ID was originally created withidFromName(). - Names longer than 1,024 bytes are not passed through to
ctx.id. - The Durable Object was created with
newUniqueId().
:::note[Alarms]
ctx.id.name is especially useful inside alarm handlers, where there is no calling client to pass the name as an argument. When the alarm fires, ctx.id.name holds the same name the object was originally accessed with.
Alarms created before 2026-03-15 do not have name stored. When such an alarm fires, ctx.id.name will be undefined, and any new alarm scheduled from that handler will also lack a name. To fix this, reschedule the alarm from a fetch() or RPC handler where name is available.
:::
const uniqueId = env.MY_DURABLE_OBJECT.newUniqueId();
const fromNameId = env.MY_DURABLE_OBJECT.idFromName("foo");
console.assert(uniqueId.name === undefined, "unique ids have no name");
console.assert(
fromNameId.name === "foo",
"name matches parameter to idFromName",
);
const uniqueId: DurableObjectId = env.MY_DURABLE_OBJECT.newUniqueId();
const fromNameId: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName("foo");
console.assert(uniqueId.name === undefined, "unique ids have no name");
console.assert(
fromNameId.name === "foo",
"name matches parameter to idFromName",
);
unique_id = env.MY_DURABLE_OBJECT.newUniqueId()
from_name_id = env.MY_DURABLE_OBJECT.idFromName("foo")
assert unique_id.name is None, "unique ids have no name"
assert from_name_id.name == "foo", "name matches parameter to idFromName"
The same name is available inside the Durable Object via ctx.id.name:
import { DurableObject } from "cloudflare:workers";
export class ChatRoom extends DurableObject {
async getRoomName() {
return this.ctx.id.name; // "foo" when accessed via getByName("foo")
}
}
import { DurableObject } from "cloudflare:workers";
export class ChatRoom extends DurableObject<Env> {
async getRoomName(): Promise<string | undefined> {
return this.ctx.id.name; // "foo" when accessed via getByName("foo")
}
}
from workers import DurableObject
class ChatRoom(DurableObject):
async def get_room_name(self):
return self.ctx.id.name # "foo" when accessed via get_by_name("foo")
jurisdiction
jurisdiction is an optional property of a DurableObjectId, which returns the jurisdiction the ID is restricted to, such as "eu" or "fedramp". The same value is available inside the Durable Object via ctx.id.jurisdiction, including in alarm handlers and objects accessed via idFromString(), so you can make region-aware decisions without passing the jurisdiction as an argument or persisting it in storage.
jurisdiction is preserved across every ID-construction path, including:
- IDs created from a jurisdiction-restricted subnamespace, for example
env.MY_DURABLE_OBJECT.jurisdiction("eu").idFromName("foo")or.newUniqueId(). - IDs created via
env.MY_DURABLE_OBJECT.newUniqueId({ jurisdiction: "eu" }). - IDs restored from a string via
idFromString()— the jurisdiction is encoded in the string itself, so it works on any namespace binding.
ctx.id.jurisdiction is undefined in two cases:
- The Durable Object was not created in a jurisdiction-restricted namespace.
- The Durable Object's alarm was scheduled before 2026-03-15. To backfill the value, reschedule the alarm from a
fetch()or RPC handler.
const plainId = env.MY_DURABLE_OBJECT.idFromName("foo");
const euId = env.MY_DURABLE_OBJECT.jurisdiction("eu").idFromName("foo");
console.assert(plainId.jurisdiction === undefined, "no jurisdiction set");
console.assert(euId.jurisdiction === "eu", "jurisdiction matches namespace");
plain_id = env.MY_DURABLE_OBJECT.idFromName("foo")
eu_id = env.MY_DURABLE_OBJECT.jurisdiction("eu").idFromName("foo")
assert plain_id.jurisdiction is None, "no jurisdiction set"
assert eu_id.jurisdiction == "eu", "jurisdiction matches namespace"
Related resources
Workers Binding API
import { DirectoryListing } from "~/components";
KV-backed Durable Object Storage (Legacy)
import { Render, Type, MetaInfo, GlossaryTooltip, TypeScriptExample, Details, Tabs, TabItem } from "~/components";
:::note This page documents the storage API for legacy KV-backed Durable Objects.
For the newer SQLite-backed Durable Object storage API, refer to SQLite-backed Durable Object Storage. :::
Access storage
Durable Objects gain access to Storage API via the DurableObjectStorage interface and accessed by the DurableObjectState::storage property. This is frequently accessed via this.ctx.storage with the ctx parameter passed to the Durable Object constructor.
The following code snippet shows you how to store and retrieve data using the Durable Object Storage API.
```ts export class Counter extends DurableObject { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); }async increment(): Promise { let value: number = (await this.ctx.storage.get("value")) || 0; value += 1; await this.ctx.storage.put("value", value); return value; } }
</TypeScriptExample>
<TabItem label="Python" icon="seti:python">
```python
from workers import DurableObject
class Counter(DurableObject):
def __init__(self, ctx, env):
super().__init__(ctx, env)
async def increment(self):
value = (await self.ctx.storage.get("value")) or 0
value += 1
await self.ctx.storage.put("value", value)
return value
JavaScript is a single-threaded and event-driven programming language. This means that JavaScript runtimes, by default, allow requests to interleave with each other which can lead to concurrency bugs. The Durable Objects runtime uses a combination of input gates and output gates to avoid this type of concurrency bug when performing storage operations. Learn more in our blog post.
Asynchronous KV API
KV-backed Durable Objects provide KV API methods which are asynchronous.
Alarms
Other
Related resources
- Durable Objects: Easy, Fast, Correct Choose Three
- Zero-latency SQLite storage in every Durable Object blog
- WebSockets API
Durable Object Namespace
import { Render, Tabs, TabItem, GlossaryTooltip } from "~/components";
Description
A Durable Object namespace is a set of Durable Objects that are backed by the same Durable Object class. There is only one Durable Object namespace per class. A Durable Object namespace can contain any number of Durable Objects.
The DurableObjectNamespace interface is used to obtain a reference to new or existing Durable Objects. The interface is accessible from the fetch handler on a Cloudflare Worker via the env parameter, which is the standard interface when referencing bindings declared in the Wrangler configuration file.
This interface defines several methods that can be used to create an ID for a Durable Object. Note that creating an ID for a Durable Object does not create the Durable Object. The Durable Object is created lazily after calling DurableObjectNamespace::get to create a DurableObjectStub from a DurableObjectId. This ensures that objects are not constructed until they are actually accessed.
import { DurableObject } from "cloudflare:workers";
// Durable Object
export class MyDurableObject extends DurableObject {
...
}
// Worker
export default {
async fetch(request, env) {
// A stub is a client Object used to invoke methods defined by the Durable Object
const stub = env.MY_DURABLE_OBJECT.getByName("foo");
...
}
}
import { DurableObject } from "cloudflare:workers";
export interface Env {
MY_DURABLE_OBJECT: DurableObjectNamespace<MyDurableObject>;
}
// Durable Object
export class MyDurableObject extends DurableObject {
...
}
// Worker
export default {
async fetch(request, env) {
// A stub is a client Object used to invoke methods defined by the Durable Object
const stub = env.MY_DURABLE_OBJECT.getByName("foo");
...
}
} satisfies ExportedHandler<Env>;
from workers import DurableObject, WorkerEntrypoint
# Durable Object
class MyDurableObject(DurableObject):
pass
# Worker
class Default(WorkerEntrypoint):
async def fetch(self, request):
# A stub is a client Object used to invoke methods defined by the Durable Object
stub = self.env.MY_DURABLE_OBJECT.getByName("foo")
# ...
Methods
idFromName
idFromName creates a unique DurableObjectId which refers to an individual instance of the Durable Object class. Named Durable Objects are the most common method of referring to Durable Objects.
const fooId = env.MY_DURABLE_OBJECT.idFromName("foo");
const barId = env.MY_DURABLE_OBJECT.idFromName("bar");
Parameters
- A required string to be used to generate a
DurableObjectIdcorresponding to the name of a Durable Object.
Return values
- A
DurableObjectIdreferring to an instance of a Durable Object class.
newUniqueId
newUniqueId creates a randomly generated and unique DurableObjectId which refers to an individual instance of the Durable Object class. IDs created using newUniqueId, will need to be stored as a string in order to refer to the same Durable Object again in the future. For example, the ID can be stored in Workers KV, another Durable Object, or in a cookie in the user's browser.
const id = env.MY_DURABLE_OBJECT.newUniqueId();
const euId = env.MY_DURABLE_OBJECT.newUniqueId({ jurisdiction: "eu" });
:::note[newUniqueId results in lower request latency at first use]
The first time you get a Durable Object stub based on an ID derived from a name, the system has to take into account the possibility that a Worker on the opposite side of the world could have coincidentally accessed the same named Durable Object at the same time. To guarantee that only one instance of the Durable Object is created, the system must check that the Durable Object has not been created anywhere else. Due to the inherent limit of the speed of light, this round-the-world check can take up to a few hundred milliseconds. newUniqueId can skip this check.
After this first use, the location of the Durable Object will be cached around the world so that subsequent lookups are faster.
:::
Parameters
- An optional object with the key
jurisdictionand value of a jurisdiction string.
Return values
- A
DurableObjectIdreferring to an instance of the Durable Object class.
idFromString
idFromString creates a DurableObjectId from a previously generated ID that has been converted to a string. This method throws an exception if the ID is invalid, for example, if the ID was not created from the same DurableObjectNamespace.
Parameters
- A required string corresponding to a
DurableObjectIdpreviously generated either bynewUniqueIdoridFromName.
Return values
- A
DurableObjectIdreferring to an instance of a Durable Object class.
get
get obtains a DurableObjectStub from a DurableObjectId which can be used to invoke methods on a Durable Object.
This method returns the stub immediately, often before a connection has been established to the Durable Object. This allows requests to be sent to the instance right away, without waiting for a network round trip.
const id = env.MY_DURABLE_OBJECT.newUniqueId();
const stub = env.MY_DURABLE_OBJECT.get(id);
Parameters
- A required
DurableObjectId - An optional object with the key
locationHintand value of a locationHint string.
Return values
- A
DurableObjectStubreferring to an instance of a Durable Object class.
getByName
getByName obtains a DurableObjectStub from a provided name, which can be used to invoke methods on a Durable Object.
This method returns the stub immediately, often before a connection has been established to the Durable Object. This allows requests to be sent to the instance right away, without waiting for a network round trip.
const fooStub = env.MY_DURABLE_OBJECT.getByName("foo");
const barStub = env.MY_DURABLE_OBJECT.getByName("bar");
Parameters
- A required string to be used to generate a
DurableObjectStubcorresponding to an instance of the Durable Object class with the provided name. - An optional object with the key
locationHintand value of a locationHint string.
Return values
- A
DurableObjectStubreferring to an instance of a Durable Object class.
jurisdiction
jurisdiction creates a subnamespace from a namespace where all Durable Object IDs and references created from that subnamespace will be restricted to the specified jurisdiction.
const subnamespace = env.MY_DURABLE_OBJECT.jurisdiction("eu");
const euStub = subnamespace.getByName("foo");
Parameters
- A required jurisdiction string.
Return values
- A
DurableObjectNamespacescoped to a particular regulatory or geographic jurisdiction. Additional geographic jurisdictions are continuously evaluated, so share requests in the Durable Objects Discord channel.
Related resources
SQLite-backed Durable Object Storage
import { Render, Type, MetaInfo, GlossaryTooltip, TypeScriptExample, Details, Tabs, TabItem } from "~/components";
:::note This page documents the storage API for the newer SQLite-backed Durable Objects.
For the legacy KV-backed Durable Object storage API, refer to KV-backed Durable Object Storage (Legacy). :::
Access storage
Durable Objects gain access to Storage API via the DurableObjectStorage interface and accessed by the DurableObjectState::storage property. This is frequently accessed via this.ctx.storage with the ctx parameter passed to the Durable Object constructor.
The following code snippet shows you how to store and retrieve data using the Durable Object Storage API.
```ts export class Counter extends DurableObject { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); }async increment(): Promise<number> {
let value: number = (await this.ctx.storage.get('value')) || 0;
value += 1;
await this.ctx.storage.put('value', value);
return value;
}
}
</TypeScriptExample>
<TabItem label="Python" icon="seti:python">
```python
from workers import DurableObject
class Counter(DurableObject):
def __init__(self, ctx, env):
super().__init__(ctx, env)
async def increment(self):
value = (await self.ctx.storage.get('value')) or 0
value += 1
await self.ctx.storage.put('value', value)
return value
JavaScript is a single-threaded and event-driven programming language. This means that JavaScript runtimes, by default, allow requests to interleave with each other which can lead to concurrency bugs. The Durable Objects runtime uses a combination of input gates and output gates to avoid this type of concurrency bug when performing storage operations. Learn more in our blog post.
SQL API
The SqlStorage interface encapsulates methods that modify the SQLite database embedded within a Durable Object. The SqlStorage interface is accessible via the sql property of DurableObjectStorage class.
For example, using sql.exec() a user can create a table and insert rows.
import { DurableObject } from "cloudflare:workers";
export class MyDurableObject extends DurableObject {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
this.sql.exec(`
CREATE TABLE IF NOT EXISTS artist(
artistid INTEGER PRIMARY KEY,
artistname TEXT
);
INSERT INTO artist (artistid, artistname) VALUES
(123, 'Alice'),
(456, 'Bob'),
(789, 'Charlie');
`);
}
}
from workers import DurableObject
class MyDurableObject(DurableObject):
def __init__(self, ctx, env):
super().__init__(ctx, env)
self.sql = ctx.storage.sql
self.sql.exec("""
CREATE TABLE IF NOT EXISTS artist(
artistid INTEGER PRIMARY KEY,
artistname TEXT
);
INSERT INTO artist (artistid, artistname) VALUES
(123, 'Alice'),
(456, 'Bob'),
(789, 'Charlie');
""")
- SQL API methods accessed with
ctx.storage.sqlare only allowed on Durable Object classes with SQLite storage backend and will return an error if called on Durable Object classes with a KV-storage backend. - When writing data, every row update of an index counts as an additional row. However, indexes may be beneficial for read-heavy use cases. Refer to Index for SQLite Durable Objects.
- Writing data to SQLite virtual tables also counts towards rows written.
<Render file="d1-do-supported-sqlite-extensions" product="d1" params={{ product: "Durable Objects support" }}/>
exec
exec(query: , ...bindings: ):
Parameters
query:- The SQL query string to be executed.
querycan contain?placeholders for parameter bindings. Multiple SQL statements, separated with a semicolon, can be executed in thequery. With multiple SQL statements, any parameter bindings are applied to the last SQL statement in thequery, and the returned cursor is only for the last SQL statement.
- The SQL query string to be executed.
...bindings:- Optional variable number of arguments that correspond to the
?placeholders inquery.
- Optional variable number of arguments that correspond to the
Returns
A cursor (SqlStorageCursor) to iterate over query row results as objects. SqlStorageCursor is a JavaScript Iterable, which supports iteration using for (let row of cursor). SqlStorageCursor is also a JavaScript Iterator, which supports iteration using cursor.next().
:::caution[Consume cursors synchronously]
Although a cursor object can technically be held across an await, it does not provide a stable snapshot of the query results. A cursor resumed after an await may observe rows inserted, updated, or deleted after the cursor was created, including writes from a later implicit transaction that has not yet committed. If that later transaction rolls back, the cursor may have already returned data from a state that never became durable.
For predictable behavior, fully consume cursors synchronously before the next await, for example with .toArray() or Array.from(cursor). Treat cursors that cross await boundaries as having no snapshot isolation guarantees.
// ✅ Safe: cursor is fully consumed before any await
const rows = this.ctx.storage.sql.exec("SELECT * FROM users").toArray();
const result = await fetch("https://example.com", { method: "POST", body: JSON.stringify(rows) });
// ⚠️ No snapshot isolation: cursor may reflect changes made after it was created
const cursor = this.ctx.storage.sql.exec("SELECT * FROM users");
await fetch("https://example.com/notify");
// Rows returned here may include writes that occurred during the await,
// including uncommitted data from a later implicit transaction.
const rows = cursor.toArray();
:::
SqlStorageCursor supports the following methods:
next()- Returns an object representing the next value of the cursor. The returned object has
doneandvalueproperties adhering to the JavaScript Iterator.doneis set tofalsewhen a next value is present, andvalueis set to the next row object in the query result.doneis set totruewhen the entire cursor is consumed, and novalueis set.
- Returns an object representing the next value of the cursor. The returned object has
toArray()- Iterates through remaining cursor value(s) and returns an array of returned row objects.
one()- Returns a row object if query result has exactly one row. If query result has zero rows or more than one row,
one()throws an exception.
- Returns a row object if query result has exactly one row. If query result has zero rows or more than one row,
raw():- Returns an Iterator over the same query results, with each row as an array of column values (with no column names) rather than an object.
- Returned Iterator supports
next()andtoArray()methods above. - Returned cursor and
raw()iterator iterate over the same query results and can be combined. For example:
let cursor = this.sql.exec("SELECT * FROM artist ORDER BY artistname ASC;");
let rawResult = cursor.raw().next();
if (!rawResult.done) {
console.log(rawResult.value); // prints [ 123, 'Alice' ]
} else {
// query returned zero results
}
console.log(cursor.toArray()); // prints [{ artistid: 456, artistname: 'Bob' },{ artistid: 789, artistname: 'Charlie' }]
cursor = self.sql.exec("SELECT * FROM artist ORDER BY artistname ASC;")
raw_result = cursor.raw().next()
if not raw_result.done:
print(raw_result.value) # prints [ 123, 'Alice' ]
else:
# query returned zero results
pass
print(cursor.toArray()) # prints [{ artistid: 456, artistname: 'Bob' },{ artistid: 789, artistname: 'Charlie' }]
SqlStorageCursor has the following properties:
columnNames:- The column names of the query in the order they appear in each row array returned by the
rawiterator.
- The column names of the query in the order they appear in each row array returned by the
rowsRead:- The number of rows read so far as part of this SQL
query. This may increase as you iterate the cursor. The final value is used for SQL billing.
- The number of rows read so far as part of this SQL
rowsWritten:- The number of rows written so far as part of this SQL
query. This may increase as you iterate the cursor. The final value is used for SQL billing.
- The number of rows written so far as part of this SQL
- Any numeric value in a column is affected by JavaScript's 52-bit precision for numbers. If you store a very large number (in
int64), then retrieve the same value, the returned value may be less precise than your original number.
:::note[SQL transactions]
Note that sql.exec() cannot execute transaction-related statements like BEGIN TRANSACTION or SAVEPOINT. Instead, use the ctx.storage.transaction() or ctx.storage.transactionSync() APIs to start a transaction, and then execute SQL queries in your callback.
:::
Examples
databaseSize
databaseSize:
Returns
The current SQLite database size in bytes.
let size = ctx.storage.sql.databaseSize;
size = ctx.storage.sql.databaseSize
PITR (Point In Time Recovery) API
For SQLite-backed Durable Objects, the following point-in-time-recovery (PITR) API methods are available to restore a Durable Object's embedded SQLite database to any point in time in the past 30 days. These methods apply to the entire SQLite database contents, including both the object's stored SQL data and stored key-value data using the key-value put() API. The PITR API is not supported in local development because a durable log of data changes is not stored locally.
The PITR API represents points in time using 'bookmarks'. A bookmark is a mostly alphanumeric string like 0000007b-0000b26e-00001538-0c3e87bb37b3db5cc52eedb93cd3b96b. Bookmarks are designed to be lexically comparable: a bookmark representing an earlier point in time compares less than one representing a later point, using regular string comparison.
getCurrentBookmark
ctx.storage.getCurrentBookmark():
- Returns a bookmark representing the current point in time in the object's history.
getBookmarkForTime
ctx.storage.getBookmarkForTime(timestamp: ):
- Returns a bookmark representing approximately the given point in time, which must be within the last 30 days. If the timestamp is represented as a number, it is converted to a date as if using
new Date(timestamp).
onNextSessionRestoreBookmark
ctx.storage.onNextSessionRestoreBookmark(bookmark: ):
- Configures the Durable Object so that the next time it restarts, it should restore its storage to exactly match what the storage contained at the given bookmark. After calling this, the application should typically invoke
ctx.abort()to restart the Durable Object, thus completing the point-in-time recovery.
This method returns a special bookmark representing the point in time immediately before the recovery takes place (even though that point in time is still technically in the future). Thus, after the recovery completes, it can be undone by performing a second recovery to this bookmark.
const DAY_MS = 24*60*60*1000;
// restore to 2 days ago
let bookmark = ctx.storage.getBookmarkForTime(Date.now() - 2 * DAYS_MS);
ctx.storage.onNextSessionRestoreBookmark(bookmark);
from datetime import datetime, timedelta
now = datetime.now()
# restore to 2 days ago
bookmark = ctx.storage.getBookmarkForTime(now - timedelta(days=2))
ctx.storage.onNextSessionRestoreBookmark(bookmark)
Synchronous KV API
Asynchronous KV API
Alarms
Other
Storage properties
sql
sql is a readonly property of type DurableObjectStorage encapsulating the SQL API.
Related resources
- Durable Objects: Easy, Fast, Correct Choose Three
- Zero-latency SQLite storage in every Durable Object blog
- WebSockets API
Durable Object State
import { Tabs, TabItem, GlossaryTooltip, Type, MetaInfo } from "~/components";
Description
The DurableObjectState interface is accessible as an instance property on the Durable Object class. This interface encapsulates methods that modify the state of a Durable Object, for example which WebSockets are attached to a Durable Object or how the runtime should handle concurrent Durable Object requests.
The DurableObjectState interface is different from the Storage API in that it does not have top-level methods which manipulate persistent application data. These methods are instead encapsulated in the DurableObjectStorage interface and accessed by DurableObjectState::storage.
import { DurableObject } from "cloudflare:workers";
// Durable Object
export class MyDurableObject extends DurableObject {
// DurableObjectState is accessible via the ctx instance property
constructor(ctx, env) {
super(ctx, env);
}
...
}
import { DurableObject } from "cloudflare:workers";
export interface Env {
MY_DURABLE_OBJECT: DurableObjectNamespace<MyDurableObject>;
}
// Durable Object
export class MyDurableObject extends DurableObject {
// DurableObjectState is accessible via the ctx instance property
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}
...
}
from workers import DurableObject
# Durable Object
class MyDurableObject(DurableObject):
# DurableObjectState is accessible via the ctx instance property
def __init__(self, ctx, env):
super().__init__(ctx, env)
# ...
Methods and Properties
exports
Contains loopback bindings to the Worker's own top-level exports. This has exactly the same meaning as ExecutionContext's ctx.exports.
waitUntil
waitUntil is available on DurableObjectState for API compatibility with Workers Runtime APIs.
:::note[waitUntil can prevent eviction]
A promise passed to waitUntil prevents eviction until it settles or for up to 15 minutes, whichever comes first. This lets work continue after the client disconnects. It does not affect when a request or RPC completes. For more information, refer to Lifecycle of a Durable Object.
:::
Parameters
- A required promise of any type.
Return values
- None.
blockConcurrencyWhile
blockConcurrencyWhile executes an async callback while blocking any other events from being delivered to the Durable Object until the callback completes. This method guarantees ordering and prevents concurrent requests. All events that were not explicitly initiated as part of the callback itself will be blocked. Once the callback completes, all other events will be delivered.
blockConcurrencyWhileis commonly used within the constructor of the Durable Object class to enforce initialization to occur before any requests are delivered.- Another use case is executing
asyncoperations based on the current state of the Durable Object and usingblockConcurrencyWhileto prevent that state from changing while yielding the event loop. - If the callback throws an exception, the object will be terminated and reset. This ensures that the object cannot be left stuck in an uninitialized state if something fails unexpectedly.
- To avoid this behavior, enclose the body of your callback in a
try...catchblock to ensure it cannot throw an exception.
To help mitigate deadlocks there is a 30 second timeout applied when executing the callback. If this timeout is exceeded, the Durable Object will be reset. It is best practice to have the callback do as little work as possible to improve overall request throughput to the Durable Object.
:::note[When to use blockConcurrencyWhile]
Use blockConcurrencyWhile in the constructor to run schema migrations or initialize state before any requests are processed. This ensures your Durable Object is fully ready before handling traffic.
For regular request handling, you rarely need blockConcurrencyWhile. SQLite storage operations are synchronous and do not yield the event loop, so they execute atomically without it. For asynchronous KV storage operations, input gates already prevent other requests from interleaving during storage calls.
Reserve blockConcurrencyWhile outside the constructor for cases where you make external async calls (such as fetch()) and cannot tolerate state changes while the event loop yields.
:::
// Durable Object
export class MyDurableObject extends DurableObject {
initialized = false;
constructor(ctx, env) {
super(ctx, env);
// blockConcurrencyWhile will ensure that initialized will always be true
this.ctx.blockConcurrencyWhile(async () => {
this.initialized = true;
});
}
...
}
# Durable Object
class MyDurableObject(DurableObject):
def __init__(self, ctx, env):
super().__init__(ctx, env)
self.initialized = False
# blockConcurrencyWhile will ensure that initialized will always be true
async def set_initialized():
self.initialized = True
self.ctx.blockConcurrencyWhile(set_initialized)
# ...
Parameters
- A required callback which returns a
Promise<T>.
Return values
- A
Promise<T>returned by the callback.
acceptWebSocket
acceptWebSocket is part of the WebSocket Hibernation API, which allows a Durable Object to be removed from memory to save costs while keeping its WebSockets connected.
acceptWebSocket adds a WebSocket to the set of WebSockets attached to the Durable Object. Once called, any incoming messages will be delivered by calling the Durable Object's webSocketMessage handler, and webSocketClose will be invoked upon disconnect. After calling acceptWebSocket, the WebSocket is accepted and its send and close methods can be used.
The WebSocket Hibernation API takes the place of the standard WebSockets API. Therefore, ws.accept must not have been called separately and ws.addEventListener method will not receive events as they will instead be delivered to the Durable Object.
The WebSocket Hibernation API permits a maximum of 32,768 WebSocket connections per Durable Object, but the CPU and memory usage of a given workload may further limit the practical number of simultaneous connections.
Parameters
- A required
WebSocketwith namews. - An optional
Array<string>of associated tags. Tags can be used to retrieve WebSockets viaDurableObjectState::getWebSockets. Each tag is a maximum of 256 characters and there can be at most 10 tags associated with a WebSocket.
Return values
- None.
getWebSockets
getWebSockets is part of the WebSocket Hibernation API, which allows a Durable Object to be removed from memory to save costs while keeping its WebSockets connected.
getWebSockets returns an Array<WebSocket> which is the set of WebSockets attached to the Durable Object. An optional tag argument can be used to filter the list according to tags supplied when calling DurableObjectState::acceptWebSocket.
:::note[waitUntil is not necessary]
Disconnected WebSockets are not returned by this method, but getWebSockets may still return WebSockets even after ws.close has been called. For example, if the server-side WebSocket sends a close, but does not receive one back (and has not detected a disconnect from the client), then the connection is in the CLOSING readyState. The client might send more messages, so the WebSocket is technically not disconnected.
With the web_socket_auto_reply_to_close compatibility flag (enabled by default on compatibility dates on or after 2026-04-07), the runtime automatically completes the close handshake, so WebSockets transition from CLOSING to CLOSED much faster and are less likely to be observed in the CLOSING state.
:::
Parameters
- An optional tag of type
string.
Return values
- An
Array<WebSocket>.
setWebSocketAutoResponse
setWebSocketAutoResponse is part of the WebSocket Hibernation API, which allows a Durable Object to be removed from memory to save costs while keeping its WebSockets connected.
setWebSocketAutoResponse sets an automatic response, auto-response, for the request provided for all WebSockets attached to the Durable Object. If a request is received matching the provided request then the auto-response will be returned without waking WebSockets in hibernation and incurring billable duration charges.
setWebSocketAutoResponse is a common alternative to setting up a server for static ping/pong messages because this can be handled without waking hibernating WebSockets.
Parameters
- An optional
WebSocketRequestResponsePair(request string, response string)enabling any WebSocket accepted viaDurableObjectState::acceptWebSocketto automatically reply to the provided response when it receives the provided request. Both request and response are limited to 2,048 characters each. If the parameter is omitted, any previously set auto-response configuration will be removed.DurableObjectState::getWebSocketAutoResponseTimestampwill still reflect the last timestamp that an auto-response was sent.
Return values
- None.
getWebSocketAutoResponse
getWebSocketAutoResponse returns the WebSocketRequestResponsePair object last set by DurableObjectState::setWebSocketAutoResponse, or null if not auto-response has been set.
:::note[inspect WebSocketRequestResponsePair]
WebSocketRequestResponsePair can be inspected further by calling getRequest and getResponse methods.
:::
Parameters
- None.
Return values
- A
WebSocketRequestResponsePairor null.
getWebSocketAutoResponseTimestamp
getWebSocketAutoResponseTimestamp is part of the WebSocket Hibernation API, which allows a Durable Object to be removed from memory to save costs while keeping its WebSockets connected.
getWebSocketAutoResponseTimestamp gets the most recent Date on which the given WebSocket sent an auto-response, or null if the given WebSocket never sent an auto-response.
Parameters
- A required
WebSocket.
Return values
- A
Dateor null.
setHibernatableWebSocketEventTimeout
setHibernatableWebSocketEventTimeout is part of the WebSocket Hibernation API, which allows a Durable Object to be removed from memory to save costs while keeping its WebSockets connected.
setHibernatableWebSocketEventTimeout sets the maximum amount of time in milliseconds that a WebSocket event can run for.
If no parameter or a parameter of 0 is provided and a timeout has been previously set, then the timeout will be unset. The maximum value of timeout is 604,800,000 ms (7 days).
Parameters
- An optional
number.
Return values
- None.
getHibernatableWebSocketEventTimeout
getHibernatableWebSocketEventTimeout is part of the WebSocket Hibernation API, which allows a Durable Object to be removed from memory to save costs while keeping its WebSockets connected.
getHibernatableWebSocketEventTimeout gets the currently set hibernatable WebSocket event timeout if one has been set via DurableObjectState::setHibernatableWebSocketEventTimeout.
Parameters
- None.
Return values
- A number, or null if the timeout has not been set.
getTags
Parameters
- A required
WebSocket.
Return values
- An
Array<string>of tags.
abort
Calling abort immediately resets a Durable Object. The runtime logs a JavaScript Error with the message passed to abort. Application code cannot catch this error.
By default, an alarm interrupted by abort retries after the Durable Object resets. A Durable Object can run an alarm concurrently with another request, and that request can call abort while the alarm is still running.
The default retry prevents an unrelated request from permanently canceling the alarm. Pass { retryAlarm: false } on any abort call that should prevent an interrupted alarm from retrying, including abort calls outside the alarm handler.
// Durable Object
export class MyDurableObject extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}
async sayHello() {
// Error: Hello, World! will be logged
this.ctx.abort("Hello, World!");
}
async alarm() {
// Reset this instance without retrying the alarm
this.ctx.abort("Alarm complete", { retryAlarm: false });
}
}
# Durable Object
class MyDurableObject(DurableObject):
def __init__(self, ctx, env):
super().__init__(ctx, env)
async def say_hello(self):
# Error: Hello, World! will be logged
self.ctx.abort("Hello, World!")
Parameters
- An optional
stringcontaining the error message to log. - An optional
DurableObjectAbortOptionsobject:retryAlarm: Controls whether an alarm interrupted by this abort retries. It defaults totrue.
Return values
- None.
Properties
id
id is a readonly property of type DurableObjectId corresponding to the DurableObjectId of the Durable Object.
storage
storage is a readonly property of type DurableObjectStorage encapsulating the Storage API.
Related resources
Durable Object Stub
import { Render, GlossaryTooltip, Tabs, TabItem } from "~/components";
Description
The DurableObjectStub interface is a client used to invoke methods on a remote Durable Object. The type of DurableObjectStub is generic to allow for RPC methods to be invoked on the stub.
Durable Objects implement E-order semantics, a concept deriving from the E distributed programming language. When you make multiple calls to the same Durable Object, it is guaranteed that the calls will be delivered to the remote Durable Object in the order in which you made them. E-order semantics makes many distributed programming problems easier. E-order is implemented by the Cap'n Proto distributed object-capability RPC protocol, which Cloudflare Workers uses for internal communications.
If an exception is thrown by a Durable Object stub all in-flight calls and future calls will fail with exceptions. To continue invoking methods on a remote Durable Object a Worker must recreate the stub. There are no ordering guarantees between different stubs.
Properties
id
id is a property of the DurableObjectStub corresponding to the DurableObjectId used to create the stub.
const id = env.MY_DURABLE_OBJECT.newUniqueId();
const stub = env.MY_DURABLE_OBJECT.get(id);
console.assert(id.equals(stub.id), "This should always be true");
id = env.MY_DURABLE_OBJECT.newUniqueId()
stub = env.MY_DURABLE_OBJECT.get(id)
assert id.equals(stub.id), "This should always be true"
name
name is an optional property of a DurableObjectStub, which returns a name if it was provided upon stub creation either directly via DurableObjectNamespace::getByName or indirectly via a DurableObjectId created by DurableObjectNamespace::idFromName. This value is undefined if the DurableObjectId used to create the DurableObjectStub was constructed using DurableObjectNamespace::newUniqueId.
const stub = env.MY_DURABLE_OBJECT.getByName("foo");
console.assert(stub.name === "foo", "This should always be true");
stub = env.MY_DURABLE_OBJECT.getByName("foo")
assert stub.name == "foo", "This should always be true"
Related resources
WebGPU
:::caution
The WebGPU API is only available in local development. You cannot deploy Durable Objects to Cloudflare that rely on the WebGPU API. See Workers AI for information on running machine learning models on the GPUs in Cloudflare's global network.
:::
The WebGPU API allows you to use the GPU directly from JavaScript.
The WebGPU API is only accessible from within Durable Objects. You cannot use the WebGPU API from within Workers.
To use the WebGPU API in local development, enable the experimental and webgpu compatibility flags in the Wrangler configuration file of your Durable Object.
compatibility_flags = ["experimental", "webgpu"]
The following subset of the WebGPU API is available from within Durable Objects:
The following subset of the WebGPU API is not yet supported:
| API | Supported? | Notes |
|---|---|---|
GPU.getPreferredCanvasFormat |
||
GPURenderBundle |
||
GPURenderBundleEncoder |
||
GPURenderPassEncoder |
||
GPURenderPipeline |
||
GPUShaderModule |
||
GPUTexture |
||
GPUTextureView |
||
GPUExternalTexture |
Examples
- workers-wonnx — Image classification, running on a GPU via the WebGPU API, using the wonnx model inference runtime.
Access Durable Objects Storage
import { Render, GlossaryTooltip, WranglerConfig } from "~/components";
Durable Objects are a powerful compute API that provides a compute with storage building block. Each Durable Object has its own private, transactional, and strongly consistent storage. Durable Objects Storage API provides access to a Durable Object's attached storage.
A Durable Object's in-memory state is preserved as long as the Durable Object remains in memory. Inactive Durable Objects with no incoming request traffic can be evicted.
When you deploy a code change to Durable Objects, the default code update strategy is deferred, with a max_delay of 300 seconds (5 minutes). Active objects can continue running their current code until they hibernate or reach this delay. When an object starts on the new version, Cloudflare creates a fresh isolate and discards any in-memory state.
Deferral gives your application time to finish in-flight work or persist in-memory state to SQLite or external storage. It is best effort, not a guarantee that every operation finishes. Cloudflare can reset an object before max_delay is reached.
Just like other compute environments where compute instances do not live forever, you should persist any state that must be durable across code deployments to Durable Storage. Durable Objects makes this simple — each individual Durable Object has its own SQLite database that you can read from and write to synchronously.
Access storage
Storage API methods are available on ctx.storage parameter passed to the Durable Object constructor. Storage API has several methods, including SQL, point-in-time recovery (PITR), key-value (KV), and alarm APIs.
Only Durable Object classes with a SQLite storage backend can access SQL API.
Create SQLite-backed Durable Object class
Use new_sqlite_classes on the migration in your Worker's Wrangler file:
{
"migrations": [
{
"tag": "v1", // Should be unique for each entry
"new_sqlite_classes": [ // Array of new classes
"MyDurableObject"
]
}
]
}
SQL API is available on ctx.storage.sql parameter passed to the Durable Object constructor.
SQLite-backed Durable Objects also offer point-in-time recovery API, which uses bookmarks to allow you to restore a Durable Object's embedded SQLite database to any point in time in the past 30 days.
Initialize instance variables from storage
A common pattern is to initialize a Durable Object from persistent storage and set instance variables the first time it is accessed. Since future accesses are routed to the same Durable Object, it is then possible to return any initialized values without making further calls to persistent storage.
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject {
value: number;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// `blockConcurrencyWhile()` ensures no requests are delivered until
// initialization completes.
ctx.blockConcurrencyWhile(async () => {
// After initialization, future reads do not need to access storage.
this.value = (await ctx.storage.get("value")) || 0;
});
}
async getCounterValue() {
return this.value;
}
}
Remove a Durable Object's storage
A Durable Object fully ceases to exist if, when it shuts down, its storage is empty. If you never write to a Durable Object's storage at all (including setting alarms), then storage remains empty, and so the Durable Object will no longer exist once it shuts down.
However if you ever write using Storage API, including setting alarms, then you must explicitly call storage.deleteAll() to empty storage and storage.deleteAlarm() if you've configured an alarm. It is not sufficient to simply delete the specific data that you wrote, such as deleting a key or dropping a table, as some metadata may remain. The only way to remove all storage is to call deleteAll(). Calling deleteAll() ensures that a Durable Object will not be billed for storage.
export class MyDurableObject extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}
// Clears Durable Object storage
async clearDo(): Promise<void> {
// If you've configured a Durable Object alarm
await this.ctx.storage.deleteAlarm();
// This will delete all the storage associated with this Durable Object instance
// This will also delete the Durable Object instance itself
await this.ctx.storage.deleteAll();
}
}
SQL API Examples
TypeScript and query results
You can use TypeScript type parameters to provide a type for your results, allowing you to benefit from type hints and checks when iterating over the results of a query.
:::caution
Providing a type parameter does not validate that the query result matches your type definition. In TypeScript, properties (fields) that do not exist in your result type will be silently dropped.
:::
Your type must conform to the shape of a TypeScript Record type representing the name (string) of the column and the type of the column. The column type must be a valid SqlStorageValue: one of ArrayBuffer | string | number | null.
For example,
type User = {
id: string;
name: string;
email_address: string;
version: number;
};
This type can then be passed as the type parameter to a sql.exec() call:
// The type parameter is passed between angle brackets before the function argument:
const result = this.ctx.storage.sql
.exec<User>(
"SELECT id, name, email_address, version FROM users WHERE id = ?",
user_id,
)
.one();
// result will now have a type of "User"
// Alternatively, if you are iterating over results using a cursor
let cursor = this.sql.exec<User>(
"SELECT id, name, email_address, version FROM users WHERE id = ?",
user_id,
);
for (let row of cursor) {
// Each row object will be of type User
}
// Or, if you are using raw() to convert results into an array, define an array type:
type UserRow = [
id: string,
name: string,
email_address: string,
version: number,
];
// ... and then pass it as the type argument to the raw() method:
let cursor = sql
.exec(
"SELECT id, name, email_address, version FROM users WHERE id = ?",
user_id,
)
.raw<UserRow>();
for (let row of cursor) {
// row is of type User
}
You can represent the shape of any result type you wish, including more complex types. If you are performing a
JOIN across multiple tables, you can compose a type that reflects the results of your queries.
Indexes in SQLite
Creating indexes for your most queried tables and filtered columns reduces how much data is scanned and improves query performance at the same time. If you have a read-heavy workload (most common), this can be particularly advantageous. Writing to columns referenced in an index will add at least one (1) additional row written to account for updating the index, but this is typically offset by the reduction in rows read due to the benefits of an index.
Related resources
Invoke methods
import { Render, Tabs, TabItem, GlossaryTooltip } from "~/components";
Invoking methods on a Durable Object
All new projects and existing projects with a compatibility date greater than or equal to 2024-04-03 should prefer to invoke Remote Procedure Call (RPC) methods defined on a Durable Object class.
Projects requiring HTTP request/response flows or legacy projects can continue to invoke the fetch() handler on the Durable Object class.
Invoke RPC methods
By writing a Durable Object class which inherits from the built-in type DurableObject, public methods on the Durable Objects class are exposed as RPC methods, which you can call using a DurableObjectStub from a Worker.
All RPC calls are asynchronous, accept and return serializable types, and propagate exceptions to the caller without a stack trace. Refer to Workers RPC for complete details.
:::note
With RPC, the DurableObject superclass defines ctx and env as class properties. What was previously called state is now called ctx when you extend the DurableObject class. The name ctx is adopted rather than state for the DurableObjectState interface to be consistent between DurableObject and WorkerEntrypoint objects.
:::
Refer to Build a Counter for a complete example.
Invoking the fetch handler
If your project is stuck on a compatibility date before 2024-04-03, or has the need to send a Request object and return a Response object, then you should send requests to a Durable Object via the fetch handler.
import { DurableObject } from "cloudflare:workers";
// Durable Object
export class MyDurableObject extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
}
async fetch(request) {
return new Response("Hello, World!");
}
}
// Worker
export default {
async fetch(request, env) {
// A stub is a client used to invoke methods on the Durable Object
const stub = env.MY_DURABLE_OBJECT.getByName("foo");
// Methods on the Durable Object are invoked via the stub
const response = await stub.fetch(request);
return response;
},
};
import { DurableObject } from "cloudflare:workers";
export interface Env {
MY_DURABLE_OBJECT: DurableObjectNamespace<MyDurableObject>;
}
// Durable Object
export class MyDurableObject extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}
async fetch(request: Request): Promise<Response> {
return new Response("Hello, World!");
}
}
// Worker
export default {
async fetch(request, env) {
// A stub is a client used to invoke methods on the Durable Object
const stub = env.MY_DURABLE_OBJECT.getByName("foo");
// Methods on the Durable Object are invoked via the stub
const response = await stub.fetch(request);
return response;
},
} satisfies ExportedHandler<Env>;
The URL associated with the Request object passed to the fetch() handler of your Durable Object must be a well-formed URL, but does not have to be a publicly-resolvable hostname.
Without RPC, customers frequently construct requests which corresponded to private methods on the Durable Object and dispatch requests from the fetch handler. RPC is obviously more ergonomic in this example.
import { DurableObject } from "cloudflare:workers";
// Durable Object
export class MyDurableObject extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}
private hello(name) {
return new Response(`Hello, ${name}!`);
}
private goodbye(name) {
return new Response(`Goodbye, ${name}!`);
}
async fetch(request) {
const url = new URL(request.url);
let name = url.searchParams.get("name");
if (!name) {
name = "World";
}
switch (url.pathname) {
case "/hello":
return this.hello(name);
case "/goodbye":
return this.goodbye(name);
default:
return new Response("Bad Request", { status: 400 });
}
}
}
// Worker
export default {
async fetch(_request, env, _ctx) {
// A stub is a client used to invoke methods on the Durable Object
const stub = env.MY_DURABLE_OBJECT.getByName("foo");
// Invoke the fetch handler on the Durable Object stub
let response = await stub.fetch("http://do/hello?name=World");
return response;
},
};
import { DurableObject } from "cloudflare:workers";
export interface Env {
MY_DURABLE_OBJECT: DurableObjectNamespace<MyDurableObject>;
}
// Durable Object
export class MyDurableObject extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}
private hello(name: string) {
return new Response(`Hello, ${name}!`);
}
private goodbye(name: string) {
return new Response(`Goodbye, ${name}!`);
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
let name = url.searchParams.get("name");
if (!name) {
name = "World";
}
switch (url.pathname) {
case "/hello":
return this.hello(name);
case "/goodbye":
return this.goodbye(name);
default:
return new Response("Bad Request", { status: 400 });
}
}
}
// Worker
export default {
async fetch(_request, env, _ctx) {
// A stub is a client used to invoke methods on the Durable Object
const stub = env.MY_DURABLE_OBJECT.getByName("foo");
// Invoke the fetch handler on the Durable Object stub
let response = await stub.fetch("http://do/hello?name=World");
return response;
},
} satisfies ExportedHandler<Env>;
Error handling
import { GlossaryTooltip } from "~/components";
Any uncaught exceptions thrown by a Durable Object or thrown by Durable Objects' infrastructure (such as overloads or network errors) will be propagated to the callsite of the client. Catching these exceptions allows you to retry creating the DurableObjectStub and sending requests.
JavaScript Errors with the property .retryable set to True are suggested to be retried if requests to the Durable Object are idempotent, or can be applied multiple times without changing the response. If requests are not idempotent, then you will need to decide what is best for your application. It is strongly recommended to apply exponential backoff when retrying requests.
JavaScript Errors with the property .overloaded set to True should not be retried. If a Durable Object is overloaded, then retrying will worsen the overload and increase the overall error rate.
:::note[Recreating the DurableObjectStub after exceptions]
Many exceptions leave the DurableObjectStub in a "broken" state, such that all attempts to send additional requests will just fail immediately with the original exception. To avoid this, you should avoid reusing a DurableObjectStub after it throws an exception. You should instead create a new one for any subsequent requests.
:::
How exceptions are thrown
Durable Objects can throw exceptions in one of two ways:
- An exception can be thrown within the user code which implements a Durable Object class. The resulting exception will have a
.remoteproperty set toTruein this case. - An exception can be generated by Durable Object's infrastructure. Some sources of infrastructure exceptions include: transient internal errors, sending too many requests to a single Durable Object, and too many requests being queued due to slow or excessive I/O (external API calls or storage operations) within an individual Durable Object. Some infrastructure exceptions may also have the
.remoteproperty set toTrue-- for example, when the Durable Object exceeds its memory or CPU limits.
Refer to Troubleshooting to review the types of errors returned by a Durable Object and/or Durable Objects infrastructure and how to prevent them.
Example
This example demonstrates retrying requests using the recommended exponential backoff algorithm.
import { DurableObject } from "cloudflare:workers";
export interface Env {
ErrorThrowingObject: DurableObjectNamespace;
}
export default {
async fetch(request, env, ctx) {
let userId = new URL(request.url).searchParams.get("userId") || "";
// Retry behavior can be adjusted to fit your application.
let maxAttempts = 3;
let baseBackoffMs = 100;
let maxBackoffMs = 20000;
let attempt = 0;
while (true) {
// Try sending the request
try {
// Create a Durable Object stub for each attempt, because certain types of
// errors will break the Durable Object stub.
const doStub = env.ErrorThrowingObject.getByName(userId);
const resp = await doStub.fetch("http://your-do/");
return Response.json(resp);
} catch (e: any) {
if (!e.retryable) {
// Failure was not a transient internal error, so don't retry.
break;
}
}
let backoffMs = Math.min(
maxBackoffMs,
baseBackoffMs * Math.random() * Math.pow(2, attempt),
);
attempt += 1;
if (attempt >= maxAttempts) {
// Reached max attempts, so don't retry.
break;
}
await scheduler.wait(backoffMs);
}
return new Response("server error", { status: 500 });
},
} satisfies ExportedHandler<Env>;
export class ErrorThrowingObject extends DurableObject {
constructor(state: DurableObjectState, env: Env) {
super(state, env);
// Any exceptions that are raised in your constructor will also set the
// .remote property to True
throw new Error("no good");
}
async fetch(req: Request) {
// Generate an uncaught exception
// A .remote property will be added to the exception propagated to the caller
// and will be set to True
throw new Error("example error");
// We never reach this
return Response.json({});
}
}
Best practices
import { DirectoryListing } from "~/components";
Rules of Durable Objects
import { WranglerConfig, TypeScriptExample, Render } from "~/components";
Durable Objects provide a powerful primitive for building stateful, coordinated applications. Each Durable Object is a single-threaded, globally-unique instance with its own persistent storage. Understanding how to design around these properties is essential for building effective applications.
This is a guidebook on how to build more effective and correct Durable Object applications.
When to use Durable Objects
Use Durable Objects for stateful coordination, not stateless request handling
Workers are stateless functions: each request may run on a different instance, in a different location, with no shared memory between requests. Durable Objects are stateful compute: each instance has a unique identity, runs in a single location, and maintains state across requests.
Use Durable Objects when you need:
- Coordination — Multiple clients need to interact with shared state (chat rooms, multiplayer games, collaborative documents)
- Strong consistency — Operations must be serialized to avoid race conditions (inventory management, booking systems, turn-based games)
- Per-entity storage — Each user, tenant, or resource needs its own isolated database (multi-tenant SaaS, per-user data)
- Persistent connections — Long-lived WebSocket connections that survive across requests (real-time notifications, live updates)
- Scheduled work per entity — Each entity needs its own timer or scheduled task (subscription renewals, game timeouts)
Use plain Workers when you need:
- Stateless request handling — API endpoints, proxies, or transformations with no shared state
- Maximum global distribution — Requests should be handled at the nearest edge location
- High fan-out — Each request is independent and can be processed in parallel
export interface Env { BOOKING: DurableObjectNamespace; }
// ✅ Good use of Durable Objects: Seat booking requires coordination // All booking requests for a venue must be serialized to prevent double-booking export class SeatBooking extends DurableObject { async bookSeat( seatId: string, userId: string ): Promise<{ success: boolean; message: string }> { // Check if seat is already booked const existing = this.ctx.storage.sql .exec<{ user_id: string }>( "SELECT user_id FROM bookings WHERE seat_id = ?", seatId ) .toArray();
if (existing.length > 0) {
return { success: false, message: "Seat already booked" };
}
// Book the seat - this is safe because Durable Objects are single-threaded
this.ctx.storage.sql.exec(
"INSERT INTO bookings (seat_id, user_id, booked_at) VALUES (?, ?, ?)",
seatId,
userId,
Date.now()
);
return { success: true, message: "Seat booked successfully" };
}
}
export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); const eventId = url.searchParams.get("event") ?? "default";
// Route to a Durable Object by event ID
// All bookings for the same event go to the same instance
const id = env.BOOKING.idFromName(eventId);
const booking = env.BOOKING.get(id);
const { seatId, userId } = await request.json<{
seatId: string;
userId: string;
}>();
const result = await booking.bookSeat(seatId, userId);
return Response.json(result, {
status: result.success ? 200 : 409,
});
},
};
</TypeScriptExample>
A common pattern is to use Workers as the stateless entry point that routes requests to Durable Objects when coordination is needed. The Worker handles authentication, validation, and response formatting, while the Durable Object handles the stateful logic.
## Design and sharding
### Model your Durable Objects around your "atom" of coordination
The most important design decision is choosing what each Durable Object represents. Create one Durable Object per logical unit that needs coordination: a chat room, a game session, a document, a user's data, or a tenant's workspace.
This is the key insight that makes Durable Objects powerful. Instead of a shared database with locks, each "atom" of your application gets its own single-threaded execution environment with private storage.
<TypeScriptExample filename="index.ts">
```ts
import { DurableObject } from "cloudflare:workers";
export interface Env {
CHAT_ROOM: DurableObjectNamespace<ChatRoom>;
}
// Each chat room is its own Durable Object instance
export class ChatRoom extends DurableObject<Env> {
async sendMessage(userId: string, message: string) {
// All messages to this room are processed sequentially by this single instance.
// No race conditions, no distributed locks needed.
this.ctx.storage.sql.exec(
"INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",
userId,
message,
Date.now()
);
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const roomId = url.searchParams.get("room") ?? "lobby";
// Each room ID maps to exactly one Durable Object instance globally
const id = env.CHAT_ROOM.idFromName(roomId);
const stub = env.CHAT_ROOM.get(id);
await stub.sendMessage("user-123", "Hello, room!");
return new Response("Message sent");
},
};
:::note
If you have global application or user configuration that you need to access frequently (on every request), consider using Workers KV instead.
:::
Do not create a single "global" Durable Object that handles all requests:
```ts import { DurableObject } from "cloudflare:workers";export interface Env { CHAT_ROOM: DurableObjectNamespace; }
// 🔴 Bad: A single Durable Object handling ALL chat rooms export class ChatRoom extends DurableObject { async sendMessage(roomId: string, userId: string, message: string) { // All messages for ALL rooms go through this single instance. // This becomes a bottleneck as traffic grows. this.ctx.storage.sql.exec( "INSERT INTO messages (room_id, user_id, content) VALUES (?, ?, ?)", roomId, userId, message ); } }
export default { async fetch(request: Request, env: Env): Promise { // 🔴 Bad: Always using the same ID means one global instance const id = env.CHAT_ROOM.idFromName("global"); const stub = env.CHAT_ROOM.get(id);
await stub.sendMessage("room-123", "user-456", "Hello!");
return new Response("Sent");
},
};
</TypeScriptExample>
### Message throughput limits
A single Durable Object can handle approximately **500-1,000 requests per second** for simple operations. This limit varies based on the work performed per request:
| Operation type | Throughput |
|----------------|------------|
| Simple pass-through (minimal parsing) | ~1,000 req/sec |
| Moderate processing (JSON parsing, validation) | ~500-750 req/sec |
| Complex operations (transformation, storage writes) | ~200-500 req/sec |
When modeling your "atom," factor in the expected request rate. If your use case exceeds these limits, shard your workload across multiple Durable Objects.
For example, consider a real-time game with 50,000 concurrent players sending 10 updates per second. This generates 500,000 requests per second total. You would need 500-1,000 game session Durable Objects—not one global coordinator.
Calculate your sharding requirements:
Required DOs = (Total requests/second) / (Requests per DO capacity)
### Use deterministic IDs for predictable routing
Use `getByName()` with meaningful, deterministic strings for consistent routing. The same input always produces the same Durable Object ID, ensuring requests for the same logical entity always reach the same instance.
<TypeScriptExample filename="index.ts">
```ts
import { DurableObject } from "cloudflare:workers";
export interface Env {
GAME_SESSION: DurableObjectNamespace<GameSession>;
}
export class GameSession extends DurableObject<Env> {
async join(playerId: string) {
// Game logic here
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const gameId = url.searchParams.get("game");
if (!gameId) {
return new Response("Missing game ID", { status: 400 });
}
// ✅ Good: Deterministic ID from a meaningful string
// All requests for "game-abc123" go to the same Durable Object
const stub = env.GAME_SESSION.getByName(gameId);
await stub.join("player-xyz");
return new Response("Joined game");
},
};
Creating a stub does not instantiate or wake up the Durable Object. The Durable Object is only activated when you call a method on the stub.
Use newUniqueId() only when you need a new, random instance and will store the mapping externally:
export interface Env { GAME_SESSION: DurableObjectNamespace; }
export class GameSession extends DurableObject { async join(playerId: string) { // Game logic here } }
export default { async fetch(request: Request, env: Env): Promise { // newUniqueId() creates a random ID - useful when creating new instances // You must store this ID somewhere (e.g., D1) to find it again later const id = env.GAME_SESSION.newUniqueId(); const stub = env.GAME_SESSION.get(id);
// Store the mapping: gameCode -> id.toString()
// await env.DB.prepare("INSERT INTO games (code, do_id) VALUES (?, ?)").bind(gameCode, id.toString()).run();
return Response.json({ gameId: id.toString() });
},
};
</TypeScriptExample>
### Use parent-child relationships for related entities
Do not put all your data in a single Durable Object. When you have hierarchical data (workspaces containing projects, game servers managing matches), create separate child Durable Objects for each entity. The parent coordinates and tracks children, while children handle their own state independently.
This enables parallelism: operations on different children can happen concurrently, while each child maintains its own single-threaded consistency ([read more about this pattern](/reference-architecture/diagrams/storage/durable-object-control-data-plane-pattern/)).
<TypeScriptExample filename="index.ts">
```ts
import { DurableObject } from "cloudflare:workers";
export interface Env {
GAME_SERVER: DurableObjectNamespace<GameServer>;
GAME_MATCH: DurableObjectNamespace<GameMatch>;
}
// Parent: Coordinates matches, but doesn't store match data
export class GameServer extends DurableObject<Env> {
async createMatch(matchName: string): Promise<string> {
const matchId = crypto.randomUUID();
// Store reference to the child in parent's database
this.ctx.storage.sql.exec(
"INSERT INTO matches (id, name, created_at) VALUES (?, ?, ?)",
matchId,
matchName,
Date.now()
);
// 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.
```ts import { DurableObject } from "cloudflare:workers";export interface Env { GAME_SESSION: DurableObjectNamespace; }
export class GameSession extends DurableObject { // Game session logic }
export default { async fetch(request: Request, env: Env): Promise { 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");
},
};
</TypeScriptExample>
Location hints are suggestions, not guarantees. Refer to [Data location](/durable-objects/reference/data-location/) for available regions and details.
## Storage and state
### Use SQLite-backed Durable Objects
[SQLite storage](/durable-objects/api/sqlite-storage-api/) 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:
<WranglerConfig>
```jsonc
{
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["ChatRoom"] }
]
}
Then use the SQL API in your Durable Object:
```ts import { DurableObject } from "cloudflare:workers";export interface Env { CHAT_ROOM: DurableObjectNamespace; }
type Message = { id: number; user_id: string; content: string; created_at: number; };
export class ChatRoom extends DurableObject { 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<Message[]> {
// Use type parameter for typed results
const cursor = this.ctx.storage.sql.exec<Message>(
"SELECT * FROM messages ORDER BY created_at DESC LIMIT ?",
limit
);
return cursor.toArray();
}
}
</TypeScriptExample>
Refer to [Access Durable Objects storage](/durable-objects/best-practices/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:
- [`durable-utils`](https://github.com/lambrospetrou/durable-utils#sqlite-schema-migrations) — provides a `SQLSchemaMigrations` class that tracks executed migrations both in memory and in storage.
- [`@cloudflare/actors` storage utilities](https://github.com/cloudflare/actors/blob/main/packages/storage/src/sql-schema-migrations.ts) — a reference implementation of the same pattern used by the Cloudflare Actors framework.
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:
<TypeScriptExample filename="index.ts">
```ts
import { DurableObject } from "cloudflare:workers";
export interface Env {
CHAT_ROOM: DurableObjectNamespace<ChatRoom>;
}
export class ChatRoom extends DurableObject<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.
```ts import { DurableObject } from "cloudflare:workers";export interface Env { CHAT_ROOM: DurableObjectNamespace; }
type Message = { id: number; user_id: string; content: string; created_at: number; };
export class ChatRoom extends DurableObject { // In-memory cache - fast but NOT preserved across evictions or crashes private messageCache: Message[] | null = null;
async getRecentMessages(): Promise<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<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
}
}
</TypeScriptExample>
:::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.
<TypeScriptExample filename="index.ts">
```ts
import { DurableObject } from "cloudflare:workers";
export interface Env {
CHAT_ROOM: DurableObjectNamespace<ChatRoom>;
}
export class ChatRoom extends DurableObject<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:
export interface Env { COUNTER: DurableObjectNamespace; }
export class Counter extends DurableObject { // This code is safe due to input gates async increment(): Promise { // While these storage operations execute, no other requests // can interleave - input gate blocks new events const value = (await this.ctx.storage.get("count")) ?? 0; await this.ctx.storage.put("count", value + 1); return value + 1; } }
</TypeScriptExample>
**Output gates** hold outgoing network messages (responses, fetch requests) until pending storage writes complete. This ensures clients never see confirmation of data that has not been persisted:
<TypeScriptExample filename="index.ts">
```ts
import { DurableObject } from "cloudflare:workers";
export interface Env {
CHAT_ROOM: DurableObjectNamespace<ChatRoom>;
}
export class ChatRoom extends DurableObject<Env> {
async sendMessage(userId: string, content: string): Promise<string> {
// Write to storage - don't need to await for correctness
this.ctx.storage.sql.exec(
"INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",
userId,
content,
Date.now()
);
// This response is held by the output gate until the write completes.
// The client only receives "Message sent" after data is safely persisted.
return "Message sent";
}
}
Write coalescing: Multiple storage writes without intervening await calls are automatically batched into a single atomic implicit transaction:
export interface Env { ACCOUNT: DurableObjectNamespace; }
export class Account extends DurableObject { async transfer(fromId: string, toId: string, amount: number) { // ✅ Good: These writes are coalesced into one atomic transaction this.ctx.storage.sql.exec( "UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromId ); this.ctx.storage.sql.exec( "UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toId ); this.ctx.storage.sql.exec( "INSERT INTO transfers (from_id, to_id, amount, created_at) VALUES (?, ?, ?, ?)", fromId, toId, amount, Date.now() ); // All three writes commit together atomically }
// 🔴 Bad: await on KV operations breaks coalescing
async transferBrokenKV(fromId: string, toId: string, amount: number) {
const fromBalance = (await this.ctx.storage.get<number>(`balance:${fromId}`)) ?? 0;
await this.ctx.storage.put(`balance:${fromId}`, fromBalance - amount);
// If the next write fails, the debit already committed!
const toBalance = (await this.ctx.storage.get<number>(`balance:${toId}`)) ?? 0;
await this.ctx.storage.put(`balance:${toId}`, toBalance + amount);
}
}
</TypeScriptExample>
For more details, see [Durable Objects: Easy, Fast, Correct — Choose three](https://blog.cloudflare.com/durable-objects-easy-fast-correct-choose-three/) and the [glossary](/durable-objects/reference/glossary/).
### Avoid race conditions with non-storage I/O
Input gates only protect during storage operations. Non-storage I/O like `fetch()` or writing to R2 allows other requests to interleave, which can cause race conditions:
<TypeScriptExample filename="index.ts">
```ts
import { DurableObject } from "cloudflare:workers";
export interface Env {
PROCESSOR: DurableObjectNamespace<Processor>;
}
export class Processor extends DurableObject<Env> {
// ⚠️ Potential race condition: fetch() allows interleaving
async processItem(id: string) {
const item = await this.ctx.storage.get<{ status: string }>(`item:${id}`);
if (item?.status === "pending") {
// During this fetch, other requests CAN execute and modify storage
const result = await fetch("https://api.example.com/process");
// Another request may have already processed this item!
await this.ctx.storage.put(`item:${id}`, { status: "completed" });
}
}
}
To handle this, use optimistic locking (check-and-set) patterns: read a version number before the external call, then verify it has not changed before writing.
:::note
With the legacy KV storage backend, use the transaction() method for atomic read-modify-write operations across async boundaries.
:::
Use blockConcurrencyWhile() sparingly
The blockConcurrencyWhile() method guarantees that no other events are processed until the provided callback completes, even if the callback performs asynchronous I/O. This is useful for operations that must be atomic, such as state initialization from storage in the constructor:
export interface Env { CHAT_ROOM: DurableObjectNamespace; }
export class ChatRoom extends DurableObject { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env);
// ✅ Good: Use blockConcurrencyWhile for one-time initialization
ctx.blockConcurrencyWhile(async () => {
this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY,
content TEXT
)
`);
});
}
// 🔴 Bad: Don't use blockConcurrencyWhile on every request
async sendMessageSlow(content: string) {
await this.ctx.blockConcurrencyWhile(async () => {
this.ctx.storage.sql.exec(
"INSERT INTO messages (content) VALUES (?)",
content
);
});
// If this takes ~5ms, you're limited to ~200 requests/second
}
// ✅ Good: Let output gates handle consistency
async sendMessageFast(content: string) {
this.ctx.storage.sql.exec(
"INSERT INTO messages (content) VALUES (?)",
content
);
// Output gate ensures write completes before response is sent
// Other requests can be processed concurrently
}
}
</TypeScriptExample>
Because `blockConcurrencyWhile()` blocks _all_ concurrency unconditionally, it significantly reduces throughput. If each call takes ~5ms, that individual Durable Object is limited to approximately 200 requests/second. Reserve it for initialization and migrations, not regular request handling. For normal operations, rely on input/output gates and write coalescing instead.
For atomic read-modify-write operations during request handling, prefer [`transaction()`](/durable-objects/api/sqlite-storage-api/#transaction) over `blockConcurrencyWhile()`. Transactions provide atomicity for storage operations without blocking unrelated concurrent requests.
:::caution
Using `blockConcurrencyWhile()` across I/O operations (such as `fetch()`, KV, R2, or other external API calls) is an anti-pattern. This is equivalent to holding a lock across I/O in other languages or concurrency frameworks — it blocks all other requests while waiting for slow external operations, severely degrading throughput. Keep `blockConcurrencyWhile()` callbacks fast and limited to local storage operations.
:::
## Communication and API design
### Use RPC methods instead of the `fetch()` handler
Projects with a [compatibility date](/workers/configuration/compatibility-flags/) of `2024-04-03` or later should use RPC methods. RPC is more ergonomic, provides better type safety, and eliminates manual request/response parsing.
Define public methods on your Durable Object class, and call them directly from stubs with full TypeScript support:
<TypeScriptExample filename="index.ts">
```ts
import { DurableObject } from "cloudflare:workers";
export interface Env {
// Type parameter provides typed method calls on the stub
CHAT_ROOM: DurableObjectNamespace<ChatRoom>;
}
type Message = {
id: number;
userId: string;
content: string;
createdAt: number;
};
export class ChatRoom extends DurableObject<Env> {
// Public methods are automatically exposed as RPC endpoints
async sendMessage(userId: string, content: string): Promise<Message> {
const createdAt = Date.now();
const result = this.ctx.storage.sql.exec<{ id: number }>(
"INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?) RETURNING id",
userId,
content,
createdAt
);
const { id } = result.one();
return { id, userId, content, createdAt };
}
async getMessages(limit: number = 50): Promise<Message[]> {
const cursor = this.ctx.storage.sql.exec<{
id: number;
user_id: string;
content: string;
created_at: number;
}>("SELECT * FROM messages ORDER BY created_at DESC LIMIT ?", limit);
return cursor.toArray().map((row) => ({
id: row.id,
userId: row.user_id,
content: row.content,
createdAt: row.created_at,
}));
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const roomId = url.searchParams.get("room") ?? "lobby";
const id = env.CHAT_ROOM.idFromName(roomId);
// stub is typed as DurableObjectStub<ChatRoom>
const stub = env.CHAT_ROOM.get(id);
if (request.method === "POST") {
const { userId, content } = await request.json<{
userId: string;
content: string;
}>();
// Direct method call with full type checking
const message = await stub.sendMessage(userId, content);
return Response.json(message);
}
// TypeScript knows getMessages() returns Promise<Message[]>
const messages = await stub.getMessages(100);
return Response.json(messages);
},
};
Refer to Invoke methods for more details on RPC and the legacy fetch() handler.
Read a Durable Object's identity from ctx.id
A Durable Object can read its own ID from this.ctx.id. If the caller accessed the Durable Object with getByName() or idFromName(), this.ctx.id.name holds that name. You do not need to pass the name as an argument or store it yourself. This also works in alarm handlers, where there is no caller to pass the name.
ctx.id.name is undefined if the Durable Object was created with newUniqueId(), was accessed with idFromString(), or was given a name longer than 1,024 bytes. Refer to Durable Object ID for details.
If your Durable Object needs application-defined data that is not part of its ID (for example, who created it), use an explicit init() method to store that data.
export interface Env { CHAT_ROOM: DurableObjectNamespace; }
export class ChatRoom extends DurableObject { // The name passed to getByName() or idFromName() getRoomId(): string { const roomId = this.ctx.id.name; if (!roomId) { throw new Error("ChatRoom must be accessed by name."); } return roomId; }
// Store application-defined data that is not part of the ID
async init(createdBy: string) {
const existing = await this.ctx.storage.get("createdBy");
if (existing) {
return; // Already initialized
}
await this.ctx.storage.put("createdBy", createdBy);
await this.ctx.storage.put("createdAt", Date.now());
}
}
export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); const roomId = url.searchParams.get("room") ?? "lobby";
const stub = env.CHAT_ROOM.getByName(roomId);
// Initialize on first access
await stub.init("system");
return new Response(`Room ${await stub.getRoomId()} ready`);
},
};
</TypeScriptExample>
### Always `await` RPC calls
When calling methods on a Durable Object stub, always use `await`. Unawaited calls create dangling promises, causing errors to be swallowed and return values to be lost.
<TypeScriptExample filename="index.ts">
```ts
import { DurableObject } from "cloudflare:workers";
export interface Env {
CHAT_ROOM: DurableObjectNamespace<ChatRoom>;
}
export class ChatRoom extends DurableObject<Env> {
async sendMessage(userId: string, content: string): Promise<number> {
const result = this.ctx.storage.sql.exec<{ id: number }>(
"INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?) RETURNING id",
userId,
content,
Date.now()
);
return result.one().id;
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const id = env.CHAT_ROOM.idFromName("lobby");
const stub = env.CHAT_ROOM.get(id);
// 🔴 Bad: Not awaiting the call
// The message ID is lost, and any errors are swallowed
stub.sendMessage("user-123", "Hello");
// ✅ Good: Properly awaited
const messageId = await stub.sendMessage("user-123", "Hello");
return Response.json({ messageId });
},
};
Error handling
Handle errors and use exception boundaries
Uncaught exceptions in a Durable Object can leave it in an unknown state and may cause the runtime to terminate the instance. Wrap risky operations in try...catch blocks, and handle errors appropriately.
export interface Env { CHAT_ROOM: DurableObjectNamespace; }
export class ChatRoom extends DurableObject { async processMessage(userId: string, content: string) { // ✅ Good: Wrap risky operations in try...catch try { // Validate input before processing if (!content || content.length > 10000) { throw new Error("Invalid message content"); }
this.ctx.storage.sql.exec(
"INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",
userId,
content,
Date.now()
);
// External call that might fail
await this.notifySubscribers(content);
} catch (error) {
// Log the error for debugging
console.error("Failed to process message:", error);
// Re-throw if it's a validation error (don't retry)
if (error instanceof Error && error.message.includes("Invalid")) {
throw error;
}
// For transient errors, you might want to handle differently
throw error;
}
}
private async notifySubscribers(content: string) {
// External notification logic
}
}
</TypeScriptExample>
When calling Durable Objects from a Worker, errors may include `.retryable` and `.overloaded` properties indicating whether the operation can be retried. For transient failures, implement exponential backoff to avoid overwhelming the system.
Refer to [Error handling](/durable-objects/best-practices/error-handling/) for details on error properties, retry strategies, and exponential backoff patterns.
## WebSockets and real-time
### Use the Hibernatable WebSockets API for cost efficiency
The Hibernatable WebSockets API allows Durable Objects to sleep while maintaining WebSocket connections. This significantly reduces costs for applications with many idle connections.
<TypeScriptExample filename="index.ts">
```ts
import { DurableObject } from "cloudflare:workers";
export interface Env {
CHAT_ROOM: DurableObjectNamespace<ChatRoom>;
}
export class ChatRoom extends DurableObject<Env> {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/websocket") {
// Check for WebSocket upgrade
if (request.headers.get("Upgrade") !== "websocket") {
return new Response("Expected WebSocket", { status: 400 });
}
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
// Accept the WebSocket with Hibernation API
this.ctx.acceptWebSocket(server);
return new Response(null, { status: 101, webSocket: client });
}
return new Response("Not found", { status: 404 });
}
// Called when a message is received (even after hibernation)
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
const data = typeof message === "string" ? message : "binary data";
// Broadcast to all connected clients
for (const client of this.ctx.getWebSockets()) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data);
}
}
}
// Called when a WebSocket is closed
async webSocketClose(
ws: WebSocket,
code: number,
reason: string,
wasClean: boolean
) {
// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
// auto-replies to Close frames. Calling close() is safe but no longer required.
ws.close(code, reason);
console.log(`WebSocket closed: ${code} ${reason}`);
}
// Called when a WebSocket error occurs
async webSocketError(ws: WebSocket, error: unknown) {
console.error("WebSocket error:", error);
}
}
With the Hibernation API, your Durable Object can go to sleep when there is no active JavaScript execution, but WebSocket connections remain open. When a message arrives, the Durable Object wakes up automatically.
Best practices:
- The WebSocket Hibernation API exposes
webSocketError,webSocketMessage, andwebSocketClosehandlers for their respective WebSocket events. - With the
web_socket_auto_reply_to_closecompatibility flag (enabled by default on compatibility dates on or after2026-04-07), the runtime automatically completes the close handshake. Callingws.close()inwebSocketCloseis still safe but no longer required. On older compatibility dates, you must callws.close()to avoid1006abnormal closure errors.
Refer to WebSockets for more details.
Use serializeAttachment() to persist per-connection state
WebSocket attachments let you store metadata for each connection that survives hibernation. Use this for user IDs, session tokens, or other per-connection data.
```ts import { DurableObject } from "cloudflare:workers";export interface Env { CHAT_ROOM: DurableObjectNamespace; }
type ConnectionState = { userId: string; username: string; joinedAt: number; };
export class ChatRoom extends DurableObject { async fetch(request: Request): Promise { const url = new URL(request.url);
if (url.pathname === "/websocket") {
if (request.headers.get("Upgrade") !== "websocket") {
return new Response("Expected WebSocket", { status: 400 });
}
const userId = url.searchParams.get("userId") ?? "anonymous";
const username = url.searchParams.get("username") ?? "Anonymous";
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
this.ctx.acceptWebSocket(server);
// Store per-connection state that survives hibernation
const state: ConnectionState = {
userId,
username,
joinedAt: Date.now(),
};
server.serializeAttachment(state);
// Broadcast join message
this.broadcast(`${username} joined the chat`);
return new Response(null, { status: 101, webSocket: client });
}
return new Response("Not found", { status: 404 });
}
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
// Retrieve the connection state (works even after hibernation)
const state = ws.deserializeAttachment() as ConnectionState;
const chatMessage = JSON.stringify({
userId: state.userId,
username: state.username,
content: message,
timestamp: Date.now(),
});
this.broadcast(chatMessage);
}
async webSocketClose(ws: WebSocket, code: number, reason: string) {
// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
// auto-replies to Close frames. Calling close() is safe but no longer required.
ws.close(code, reason);
const state = ws.deserializeAttachment() as ConnectionState;
this.broadcast(`${state.username} left the chat`);
}
private broadcast(message: string) {
for (const client of this.ctx.getWebSockets()) {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
}
}
}
</TypeScriptExample>
## Scheduling and lifecycle
### Use alarms for per-entity scheduled tasks
Each Durable Object can schedule its own future work using the [Alarms API](/durable-objects/api/alarms/), allowing a Durable Object to execute background tasks on any interval without an incoming request, RPC call, or WebSocket message.
Key points about alarms:
- **`setAlarm(timestamp)`** schedules the `alarm()` handler to run at any time in the future (millisecond precision)
- **Alarms do not repeat automatically** — you must call `setAlarm()` again to schedule the next execution
- **Only schedule alarms when there is work to do** — avoid waking up every Durable Object on short intervals (seconds), as each alarm invocation incurs costs
<TypeScriptExample filename="index.ts">
```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.
export interface Env { SUBSCRIPTION: DurableObjectNamespace; }
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);
}
}
private async processRenewal(): Promise<boolean> {
// Payment processing logic
return true;
}
}
</TypeScriptExample>
### 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](/workers/configuration/compatibility-flags/#durable-object-deleteall-deletes-alarms) and an alarm set should delete the alarm first with `deleteAlarm()`.
<TypeScriptExample filename="index.ts">
```ts
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
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:
```ts import { DurableObject } from "cloudflare:workers";export interface Env { RATE_LIMITER: DurableObjectNamespace; }
// 🔴 Bad: Global rate limiter - ALL requests go through one instance
export class RateLimiter extends DurableObject {
async checkLimit(ip: string): Promise {
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: Request, env: Env): Promise { // 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");
},
};
</TypeScriptExample>
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.
<TypeScriptExample filename="test/chat-room.test.ts">
```ts
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:
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.
Use WebSockets
import { Tabs, TabItem, GlossaryTooltip, Type, WranglerConfig, TypeScriptExample, } from "~/components";
Durable Objects can act as WebSocket servers that connect thousands of clients per instance. You can also use WebSockets as a client to connect to other servers or Durable Objects.
Two WebSocket APIs are available:
- Hibernation WebSocket API - Allows the Durable Object to hibernate without disconnecting clients when idle. (recommended)
- Web Standard WebSocket API - Uses the familiar
addEventListenerevent pattern.
What are WebSockets?
WebSockets are long-lived TCP connections that enable bi-directional, real-time communication between client and server.
Key characteristics:
- Both Workers and Durable Objects can act as WebSocket endpoints (client or server)
- WebSocket sessions are long-lived, making Durable Objects ideal for accepting connections
- A single Durable Object instance can coordinate between multiple clients (for example, chat rooms or multiplayer games)
Refer to Cloudflare Edge Chat Demo for an example of using Durable Objects with WebSockets.
Why use Hibernation?
The Hibernation WebSocket API reduces costs by allowing Durable Objects to sleep when idle:
- Clients remain connected while the Durable Object is not in memory
- Billable Duration (GB-s) charges do not accrue during hibernation
- When a message arrives, the Durable Object wakes up automatically
Durable Objects Hibernation WebSocket API
The Hibernation WebSocket API extends the Web Standard WebSocket API to reduce costs during periods of inactivity.
How hibernation works
When a Durable Object receives no events (such as alarms or messages) for a short period, it is evicted from memory. During hibernation:
- WebSocket clients remain connected to the Cloudflare network
- In-memory state is reset
- When an event arrives, the Durable Object is re-initialized and its
constructorruns
To restore state after hibernation, use serializeAttachment and deserializeAttachment to persist data with each WebSocket connection.
Refer to Lifecycle of a Durable Object for more information.
Hibernation example
To use WebSockets with Durable Objects:
- Proxy the request from the Worker to the Durable Object
- Call
DurableObjectState::acceptWebSocketto accept the server side connection - Define handler methods on the Durable Object class for relevant events
If an event occurs for a hibernated Durable Object, the runtime re-initializes it by calling the constructor. Minimize work in the constructor when using hibernation.
import { DurableObject } from "cloudflare:workers";
// Durable Object
export class WebSocketHibernationServer extends DurableObject {
async fetch(request) {
// Creates two ends of a WebSocket connection.
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
// Calling `acceptWebSocket()` connects the WebSocket to the Durable Object, allowing the WebSocket to send and receive messages.
// Unlike `ws.accept()`, `state.acceptWebSocket(ws)` allows the Durable Object to be hibernated
// When the Durable Object receives a message during Hibernation, it will run the `constructor` to be re-initialized
this.ctx.acceptWebSocket(server);
return new Response(null, {
status: 101,
webSocket: client,
});
}
async webSocketMessage(ws, message) {
// Upon receiving a message from the client, reply with the same message,
// but will prefix the message with "[Durable Object]: " and return the number of connections.
ws.send(
`[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`,
);
}
async webSocketClose(ws, code, reason, wasClean) {
// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
// auto-replies to Close frames. Calling close() is safe but no longer required.
ws.close(code, reason);
}
}
import { DurableObject } from "cloudflare:workers";
export interface Env {
WEBSOCKET_HIBERNATION_SERVER: DurableObjectNamespace<WebSocketHibernationServer>;
}
// Durable Object
export class WebSocketHibernationServer extends DurableObject {
async fetch(request: Request): Promise<Response> {
// Creates two ends of a WebSocket connection.
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
// Calling `acceptWebSocket()` connects the WebSocket to the Durable Object, allowing the WebSocket to send and receive messages.
// Unlike `ws.accept()`, `state.acceptWebSocket(ws)` allows the Durable Object to be hibernated
// When the Durable Object receives a message during Hibernation, it will run the `constructor` to be re-initialized
this.ctx.acceptWebSocket(server);
return new Response(null, {
status: 101,
webSocket: client,
});
}
async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) {
// Upon receiving a message from the client, reply with the same message,
// but will prefix the message with "[Durable Object]: " and return the number of connections.
ws.send(
`[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`,
);
}
async webSocketClose(
ws: WebSocket,
code: number,
reason: string,
wasClean: boolean,
) {
// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
// auto-replies to Close frames. Calling close() is safe but no longer required.
ws.close(code, reason);
}
}
```python
from workers import Response, DurableObject
from js import WebSocketPair
Durable Object
class WebSocketHibernationServer(DurableObject): def init(self, state, env): super().init(state, env) self.ctx = state
async def fetch(self, request):
# Creates two ends of a WebSocket connection.
client, server = WebSocketPair.new().object_values()
# Calling `acceptWebSocket()` connects the WebSocket to the Durable Object, allowing the WebSocket to send and receive messages.
# Unlike `ws.accept()`, `state.acceptWebSocket(ws)` allows the Durable Object to be hibernated
# When the Durable Object receives a message during Hibernation, it will run the `__init__` to be re-initialized
self.ctx.acceptWebSocket(server)
return Response(
None,
status=101,
web_socket=client
)
async def webSocketMessage(self, ws, message):
# Upon receiving a message from the client, reply with the same message,
# but will prefix the message with "[Durable Object]: " and return the number of connections.
ws.send(
f"[Durable Object] message: {message}, connections: {len(self.ctx.get_websockets())}"
)
async def webSocketClose(self, ws, code, reason, was_clean):
# With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
# auto-replies to Close frames. Calling close() is safe but no longer required.
ws.close(code, reason)
</TabItem>
</Tabs>
Configure your Wrangler file with a Durable Object [binding](/durable-objects/get-started/#4-configure-durable-object-bindings) and [migration](/durable-objects/reference/durable-objects-migrations/):
<WranglerConfig>
```jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "websocket-hibernation-server",
"durable_objects": {
"bindings": [
{
"name": "WEBSOCKET_HIBERNATION_SERVER",
"class_name": "WebSocketHibernationServer"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["WebSocketHibernationServer"]
}
]
}
A full example is available in Build a WebSocket server with WebSocket Hibernation.
:::caution[Local development support]
Prior to wrangler@3.13.2 and Miniflare v3.20231016.0, WebSockets did not hibernate in local development. Hibernatable WebSocket events like webSocketMessage() are still delivered. However, the Durable Object is never evicted from memory.
:::
Automatic ping/pong handling
The Cloudflare runtime automatically handles WebSocket protocol ping frames:
- Incoming ping frames receive automatic pong responses
- Ping/pong handling does not interrupt hibernation
- The
webSocketMessagehandler is not called for control frames
This behavior keeps connections alive without waking the Durable Object.
Batch messages to reduce overhead
Each WebSocket message incurs processing overhead from context switches between the JavaScript runtime and the underlying system. Sending many small messages can overwhelm a single Durable Object. This happens even if the total data volume is small.
To maximize throughput:
- Batch multiple logical messages into a single WebSocket frame
- Use a simple envelope format to pack and unpack batched messages
- Target fewer, larger messages rather than many small ones
import { DurableObject } from "cloudflare:workers";
// Define a batch envelope format
interface BatchedMessage {
messages: Array<{ type: string; payload: unknown }>;
timestamp: number;
}
// Client-side: batch messages before sending
function sendBatch(
ws: WebSocket,
messages: Array<{ type: string; payload: unknown }>,
) {
const batch: BatchedMessage = {
messages,
timestamp: Date.now(),
};
ws.send(JSON.stringify(batch));
}
// Durable Object: process batched messages
export class GameRoom extends DurableObject<Env> {
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
if (typeof message !== "string") return;
const batch = JSON.parse(message) as BatchedMessage;
// Process all messages in the batch in a single handler invocation
for (const msg of batch.messages) {
this.handleMessage(ws, msg);
}
}
private handleMessage(
ws: WebSocket,
msg: { type: string; payload: unknown },
) {
// Handle individual message logic
}
}
Why batching helps
WebSocket reads require context switches between the kernel and JavaScript runtime. Each individual message triggers this overhead. Batching 10-100 logical messages into a single WebSocket frame reduces context switches proportionally.
For high-frequency data like sensor readings or game state updates, use time-based or count-based batching. Batch every 50-100ms or every 50-100 messages, whichever comes first.
:::note
Hibernation is only supported when a Durable Object acts as a WebSocket server. Outgoing WebSockets do not hibernate.
However, an open outbound WebSocket connection prevents eviction for up to 15 minutes. The connection itself may remain open after that. Starting another I/O operation later can extend the Durable Object's time in memory. For more information, refer to Lifecycle of a Durable Object.
Events such as alarms, incoming requests, and scheduled callbacks prevent hibernation. This includes setTimeout and setInterval usage. Read more about when a Durable Object incurs duration charges.
:::
Extended methods
The following methods are available on the Hibernation WebSocket API. Use them to persist and restore state before and after hibernation.
WebSocket.serializeAttachment
serializeAttachment(value ):
Keeps a copy of value associated with the WebSocket connection.
Key behaviors:
- Serialized attachments persist through hibernation as long as the WebSocket remains healthy
- If either side closes the connection, attachments are lost
- Modifications to
valueafter calling this method are not retained unless you call it again - The
valuecan be any type supported by the structured clone algorithm - Maximum serialized size is 16,384 bytes
For larger values or data that must persist beyond WebSocket lifetime, use the Storage API and store the corresponding key as an attachment.
WebSocket.deserializeAttachment
deserializeAttachment():
Retrieves the most recent value passed to serializeAttachment(), or null if none exists.
Attachment example
Use serializeAttachment and deserializeAttachment to persist per-connection state across hibernation:
interface ConnectionState { orderId: string; joinedAt: number; }
export class WebSocketServer extends DurableObject { async fetch(request: Request): Promise { const url = new URL(request.url); const orderId = url.searchParams.get("orderId") ?? "anonymous";
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
this.ctx.acceptWebSocket(server);
// Persist per-connection state that survives hibernation
const state: ConnectionState = {
orderId,
joinedAt: Date.now(),
};
server.serializeAttachment(state);
return new Response(null, { status: 101, webSocket: client });
}
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
// Restore state after potential hibernation
const state = ws.deserializeAttachment() as ConnectionState;
ws.send(`Hello ${state.orderId}, you joined at ${state.joinedAt}`);
}
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {
const state = ws.deserializeAttachment() as ConnectionState;
console.log(`${state.orderId} disconnected`);
// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
// auto-replies to Close frames. Calling close() is safe but no longer required.
ws.close(code, reason);
}
}
</TypeScriptExample>
## WebSocket Standard API
WebSocket connections are established by making an HTTP GET request with the `Upgrade: websocket` header.
The typical flow:
1. A Worker validates the upgrade request
2. The Worker proxies the request to the Durable Object
3. The Durable Object accepts the server side connection
4. The Worker returns the client side connection in the response
:::note[Validate requests in a Worker]
Both Workers and Durable Objects are billed based on the number of requests. Validate requests in your Worker to avoid billing for invalid requests against a Durable Object.
:::
<Tabs syncKey="workersExamples">
<TabItem label="JavaScript" icon="seti:javascript">
```js
// Worker
export default {
async fetch(request, env, ctx) {
if (request.method === "GET" && request.url.endsWith("/websocket")) {
// Expect to receive a WebSocket Upgrade request.
// If there is one, accept the request and return a WebSocket Response.
const upgradeHeader = request.headers.get("Upgrade");
if (!upgradeHeader || upgradeHeader !== "websocket") {
return new Response(null, {
status: 426,
statusText: "Durable Object expected Upgrade: websocket",
headers: {
"Content-Type": "text/plain",
},
});
}
// This example will refer to a single Durable Object instance, since the name "foo" is
// hardcoded
let stub = env.WEBSOCKET_SERVER.getByName("foo");
// The Durable Object's fetch handler will accept the server side connection and return
// the client
return stub.fetch(request);
}
return new Response(null, {
status: 400,
statusText: "Bad Request",
headers: {
"Content-Type": "text/plain",
},
});
},
};
```
</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">
```ts
// Worker
export default {
async fetch(request, env, ctx): Promise<Response> {
if (request.method === "GET" && request.url.endsWith("/websocket")) {
// Expect to receive a WebSocket Upgrade request.
// If there is one, accept the request and return a WebSocket Response.
const upgradeHeader = request.headers.get("Upgrade");
if (!upgradeHeader || upgradeHeader !== "websocket") {
return new Response(null, {
status: 426,
statusText: "Durable Object expected Upgrade: websocket",
headers: {
"Content-Type": "text/plain",
},
});
}
// This example will refer to a single Durable Object instance, since the name "foo" is
// hardcoded
let stub = env.WEBSOCKET_SERVER.getByName("foo");
// The Durable Object's fetch handler will accept the server side connection and return
// the client
return stub.fetch(request);
}
return new Response(null, {
status: 400,
statusText: "Bad Request",
headers: {
"Content-Type": "text/plain",
},
});
},
} satisfies ExportedHandler<Env>;
```
</TabItem>
<TabItem label="Python" icon="seti:python">
```python
from workers import Response, WorkerEntrypoint
# Worker
class Default(WorkerEntrypoint):
async def fetch(self, request):
if request.method == "GET" and request.url.endswith("/websocket"): # Expect to receive a WebSocket Upgrade request. # If there is one, accept the request and return a WebSocket Response.
upgrade_header = request.headers.get("Upgrade")
if not upgrade_header or upgrade_header != "websocket":
return Response(
None,
status=426,
status_text="Durable Object expected Upgrade: websocket",
headers={
"Content-Type": "text/plain",
},
)
# This example will refer to a single Durable Object instance, since the name "foo" is
# hardcoded
stub = self.env.WEBSOCKET_SERVER.getByName("foo")
# The Durable Object's fetch handler will accept the server side connection and return
# the client
return await stub.fetch(request)
return Response(
None,
status=400,
status_text="Bad Request",
headers={
"Content-Type": "text/plain",
},
)
The following Durable Object creates a WebSocket connection and responds to messages with the total number of connections:
import { DurableObject } from "cloudflare:workers";
// Durable Object
export class WebSocketServer extends DurableObject {
currentlyConnectedWebSockets;
constructor(ctx, env) {
super(ctx, env);
this.currentlyConnectedWebSockets = 0;
}
async fetch(request) {
// Creates two ends of a WebSocket connection.
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
// Calling `accept()` connects the WebSocket to this Durable Object
server.accept();
this.currentlyConnectedWebSockets += 1;
// Upon receiving a message from the client, the server replies with the same message,
// and the total number of connections with the "[Durable Object]: " prefix
server.addEventListener("message", (event) => {
server.send(
`[Durable Object] currentlyConnectedWebSockets: ${this.currentlyConnectedWebSockets}`,
);
});
// When the client closes the connection, clean up the server side.
// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
// auto-replies to Close frames. Calling close() is safe but no longer required.
server.addEventListener("close", (cls) => {
this.currentlyConnectedWebSockets -= 1;
server.close(cls.code, "Durable Object is closing WebSocket");
});
return new Response(null, {
status: 101,
webSocket: client,
});
}
}
// Durable Object
export class WebSocketServer extends DurableObject {
currentlyConnectedWebSockets: number;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.currentlyConnectedWebSockets = 0;
}
async fetch(request: Request): Promise<Response> {
// Creates two ends of a WebSocket connection.
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
// Calling `accept()` connects the WebSocket to this Durable Object
server.accept();
this.currentlyConnectedWebSockets += 1;
// Upon receiving a message from the client, the server replies with the same message,
// and the total number of connections with the "[Durable Object]: " prefix
server.addEventListener("message", (event: MessageEvent) => {
server.send(
`[Durable Object] currentlyConnectedWebSockets: ${this.currentlyConnectedWebSockets}`,
);
});
// When the client closes the connection, clean up the server side.
// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
// auto-replies to Close frames. Calling close() is safe but no longer required.
server.addEventListener("close", (cls: CloseEvent) => {
this.currentlyConnectedWebSockets -= 1;
server.close(cls.code, "Durable Object is closing WebSocket");
});
return new Response(null, {
status: 101,
webSocket: client,
});
}
}
```python
from workers import Response, DurableObject
from js import WebSocketPair
from pyodide.ffi import create_proxy
Durable Object
class WebSocketServer(DurableObject): def init(self, ctx, env): super().init(ctx, env) self.currently_connected_websockets = 0
async def fetch(self, request):
# Creates two ends of a WebSocket connection.
client, server = WebSocketPair.new().object_values()
# Calling `accept()` connects the WebSocket to this Durable Object
server.accept()
self.currently_connected_websockets += 1
# Upon receiving a message from the client, the server replies with the same message,
# and the total number of connections with the "[Durable Object]: " prefix
def on_message(event):
server.send(
f"[Durable Object] currentlyConnectedWebSockets: {self.currently_connected_websockets}"
)
server.addEventListener("message", create_proxy(on_message))
# When the client closes the connection, clean up the server side.
# With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
# auto-replies to Close frames. Calling close() is safe but no longer required.
def on_close(event):
self.currently_connected_websockets -= 1
server.close(event.code, "Durable Object is closing WebSocket")
server.addEventListener("close", create_proxy(on_close))
return Response(
None,
status=101,
web_socket=client,
)
</TabItem>
</Tabs>
Configure your Wrangler file with a Durable Object [binding](/durable-objects/get-started/#4-configure-durable-object-bindings) and [migration](/durable-objects/reference/durable-objects-migrations/):
<WranglerConfig>
```jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "websocket-server",
"durable_objects": {
"bindings": [
{
"name": "WEBSOCKET_SERVER",
"class_name": "WebSocketServer"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["WebSocketServer"]
}
]
}
```
</WranglerConfig>
A full example is available in [Build a WebSocket server](/durable-objects/examples/websocket-server/).
:::caution[WebSocket disconnection during code updates]
Connections accepted with the WebSocket Hibernation API remain connected when a Durable Object adopts new code after hibernating.
A forced code update disconnects WebSockets attached to the running instance. This happens when a `deferred` code update's `max_delay` is reached before hibernation, or when the code update strategy is set to `immediate`. The Web Standard WebSocket API prevents hibernation, so its connections are disconnected when the code update is applied.
Refer to [Durable Objects code updates](/durable-objects/deployments/durable-objects-code-updates/) for more information.
:::
## Related resources
- [Mozilla Developer Network's (MDN) documentation on the WebSocket class](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
- [Cloudflare's WebSocket template for building applications on Workers using WebSockets](https://github.com/cloudflare/websocket-template)
- [Durable Object base class](/durable-objects/api/base/)
- [Durable Object State interface](/durable-objects/api/state/)
```
Lifecycle of a Durable Object
import { Render } from "~/components";
This section describes the lifecycle of a Durable Object.
To use a Durable Object you need to create a Durable Object Stub. Simply creating the Durable Object Stub does not send a request to the Durable Object, and therefore the Durable Object is not yet instantiated. A request is sent to the Durable Object and its lifecycle begins only once a method is invoked on the Durable Object Stub.
const stub = env.MY_DURABLE_OBJECT.getByName("foo");
// Now the request is sent to the remote Durable Object.
const rpcResponse = await stub.sayHello();
Durable Object Lifecycle state transitions
A Durable Object can be in one of the following states at any moment:
| State | Description |
|---|---|
| Active, in-memory | The Durable Object runs, in memory, and handles incoming requests. |
| Idle, in-memory non-hibernateable | The Durable Object waits for the next incoming request/event, but does not satisfy the criteria for hibernation. |
| Idle, in-memory hibernateable | The Durable Object waits for the next incoming request/event and satisfies the criteria for hibernation. It is up to the runtime to decide when to hibernate the Durable Object. Currently, it is after 10 seconds of inactivity while in this state. |
| Hibernated | The Durable Object is removed from memory. Hibernated WebSocket connections stay connected. |
| Inactive | The Durable Object is completely removed from the host process and might need to cold start. This is the initial state of all Durable Objects. |
This is how a Durable Object transitions among these states (each state is in a rounded rectangle).

Assuming a Durable Object does not run, the first incoming request or event (like an alarm) will execute the constructor() of the Durable Object class, then run the corresponding function invoked.
At this point the Durable Object is in the active in-memory state.
Once all incoming requests or events have been processed, the Durable Object remains idle in-memory for a few seconds either in a hibernateable state or in a non-hibernateable state.
Hibernation can only occur if all of the conditions below are true:
- No
setTimeout/setIntervalscheduled callbacks are set, since there would be no way to recreate the callback after hibernating. - No I/O operation or promise passed to
this.ctx.waitUntil()remains unfinished, and no outbound connection remains open. - No WebSocket standard API is used.
- No request/event is still being processed, because hibernating would mean losing track of the async function which is eventually supposed to return a response to that request.
After 10 seconds of no incoming request or event, and all the above conditions satisfied, the Durable Object will transition into the hibernated state.
:::caution When hibernated, the in-memory state is discarded, so ensure you persist all important information in the Durable Object's storage. :::
If any of the above conditions is false, the Durable Object remains in-memory, in the idle, in-memory, non-hibernateable state.
In case of an incoming request or event while in the hibernated state, the constructor() will run again, and the Durable Object will transition to the active, in-memory state and execute the invoked function.
While in the idle, in-memory, non-hibernateable state, after 70-140 seconds of inactivity (no incoming requests or events), the Durable Object will be evicted entirely from memory and potentially from the Cloudflare host and transition to the inactive state.
:::note[Pending operations prevent eviction]
Pending I/O and open outbound connections prevent a Durable Object from being evicted. Examples include service binding requests, Durable Object remote procedure call (RPC) calls, outbound fetch() requests, this.ctx.container.monitor(), promises passed to this.ctx.waitUntil(), and pending setTimeout() and setInterval() timers. TCP sockets and outbound WebSockets also prevent eviction.
Each operation prevents eviction until it completes or for up to 15 minutes from when it starts, whichever comes first. Operations started together do not combine their windows. Starting another operation later can extend the Durable Object's time in memory. The limit applies to each operation, not to the total time in memory.
While an operation prevents eviction, the Durable Object continues to incur duration charges. When no operation prevents eviction, the standard eviction rules resume. :::
:::note[Profiling keeps Durable Objects from being evicted] An active profiling session on a Durable Object prevents it from being evicted. While the session is running, the Durable Object cannot transition to the inactive state.
Profiling does not prevent hibernation. A Durable Object that meets the hibernation criteria can still transition to the hibernated state during a profiling session. :::
Objects in the hibernated state keep their Websocket clients connected, and the runtime decides if and when to transition the object to the inactive state (for example deciding to move the object to a different host) thus restarting the lifecycle.
The next incoming request or event starts the cycle again.
:::note[Lifecycle states incurring duration charges] A Durable Object incurs charges only when it is actively running in-memory, or when it is idle in-memory and non-hibernateable (indicated as green rectangles in the diagram). :::
Shutdown behavior
Durable Objects will occasionally shut down and objects are restarted, which will run your Durable Object class constructor. This can happen for various reasons, including:
- Worker code updates that use an
immediatecode update strategy, or adeferredstrategy that reaches itsmax_delay - Lack of requests to an object following the state transitions documented above
- Cloudflare updates to the Workers runtime system
- Workers runtime decisions on where to host objects
When a Durable Object is shut down, the object instance is automatically restarted and new requests are routed to the new instance. In-flight requests are handled as follows:
- HTTP & RPC requests: In-flight requests are allowed to finish if they do not access a Durable Object's storage. If a request attempts to access a Durable Object's storage, it will be stopped immediately and return an error to maintain Durable Objects global uniqueness property. When the Worker runtime system is being updated, in-flight requests have up to 30 seconds to complete.
- WebSocket connections: When a Durable Object shuts down, WebSocket connections are terminated automatically. This allows the new instance to take over the connection as soon as possible.
- Other invocations (email, cron): Other invocations are treated similarly to HTTP requests.
It is important to ensure that any services using Durable Objects are designed to handle the possibility of a Durable Object being shut down.
Code updates
Deploying a new Worker version is not delayed by a code update strategy — the deployment itself completes immediately. What a code update strategy controls is when each already-active Durable Object adopts that new version.
A deferred code update strategy gives an active Durable Object time to hibernate before it adopts new code. The object can continue serving requests, storage operations, and Hibernation API WebSockets on its current code. If the object is still active when max_delay is reached, it resets and adopts the new code. Deferral is best effort: it does not guarantee that every operation finishes, and the object can reset earlier.
Refer to Durable Objects code updates to configure a code update strategy and understand how it interacts with gradual deployments.
Working without shutdown hooks
Concepts
import { DirectoryListing } from "~/components";
What are Durable Objects?
import { Render } from "~/components";
Durable Objects highlights
Durable Objects have properties that make them a great fit for distributed stateful scalable applications.
Serverless compute, zero infrastructure management
- Durable Objects are built on-top of the Workers runtime, so they support exactly the same code (JavaScript and WASM), and similar memory and CPU limits.
- Each Durable Object is implicitly created on first access. User applications are not concerned with their lifecycle, creating them or destroying them. Durable Objects migrate among healthy servers, and therefore applications never have to worry about managing them.
- Each Durable Object stays alive as long as requests are being processed, and remains alive for several seconds after being idle before hibernating, allowing applications to exploit in-memory caching while handling many consecutive requests and boosting their performance.
Storage colocated with compute
- Each Durable Object has its own durable, transactional, and strongly consistent storage (up to 10 GB[^1]), persisted across requests, and accessible only within that object.
Single-threaded concurrency
- Each Durable Object instance has an identifier, either randomly-generated or user-generated, which allows you to globally address which Durable Object should handle a specific action or request.
- Durable Objects are single-threaded and cooperatively multi-tasked, just like code running in a web browser. For more details on how safety and correctness are achieved, refer to the blog post "Durable Objects: Easy, Fast, Correct — Choose three".
Elastic horizontal scaling across Cloudflare's global network
- Durable Objects can be spread around the world, and you can optionally influence where each instance should be located. Durable Objects are not yet available in every Cloudflare data center; refer to the where.durableobjects.live project for live locations.
- Each Durable Object type (or "Namespace binding" in Cloudflare terms) corresponds to a JavaScript class implementing the actual logic. There is no hard limit on how many Durable Objects can be created for each namespace.
- Durable Objects scale elastically as your application creates millions of objects. There is no need for applications to manage infrastructure or plan ahead for capacity.
Durable Objects features
In-memory state
Each Durable Object has its own in-memory state. Applications can use this in-memory state to optimize the performance of their applications by keeping important information in-memory, thereby avoiding the need to access the durable storage at all.
Useful cases for in-memory state include batching and aggregating information before persisting it to storage, or for immediately rejecting/handling incoming requests meeting certain criteria, and more.
In-memory state is reset when the Durable Object hibernates after being idle for some time. Therefore, it is important to persist any in-memory data to the durable storage if that data will be needed at a later time when the Durable Object receives another request.
Storage API
The Durable Object Storage API allows Durable Objects to access fast, transactional, and strongly consistent storage. A Durable Object's attached storage is private to its unique instance and cannot be accessed by other objects.
There are two flavors of the storage API, a key-value (KV) API and an SQL API.
When using the new SQLite in Durable Objects storage backend, you have access to both the APIs. However, if you use the previous storage backend you only have access to the key-value API.
Alarms API
Durable Objects provide an Alarms API which allows you to schedule the Durable Object to be woken up at a time in the future. This is useful when you want to do certain work periodically, or at some specific point in time, without having to manually manage infrastructure such as job scheduling runners on your own.
You can combine Alarms with in-memory state and the durable storage API to build batch and aggregation applications such as queues, workflows, or advanced data pipelines.
WebSockets
WebSockets are long-lived TCP connections that enable bi-directional, real-time communication between client and server. Because WebSocket sessions are long-lived, applications commonly use Durable Objects to accept either the client or server connection.
Because Durable Objects provide a single-point-of-coordination between Cloudflare Workers, a single Durable Object instance can be used in parallel with WebSockets to coordinate between multiple clients, such as participants in a chat room or a multiplayer game.
Durable Objects support the WebSocket Standard API, as well as the WebSockets Hibernation API which extends the Web Standard WebSocket API to reduce costs by not incurring billing charges during periods of inactivity.
RPC
Durable Objects support Workers Remote-Procedure-Call (RPC) which allows applications to use JavaScript-native methods and objects to communicate between Workers and Durable Objects.
Using RPC for communication makes application development easier and simpler to reason about, and more efficient.
Actor programming model
Another way to describe and think about Durable Objects is through the lens of the Actor programming model. There are several popular examples of the Actor model supported at the programming language level through runtimes or library frameworks, like Erlang, Elixir, Akka, or Microsoft Orleans for .NET.
The Actor model simplifies a lot of problems in distributed systems by abstracting away the communication between actors using RPC calls (or message sending) that could be implemented on-top of any transport protocol, and it avoids most of the concurrency pitfalls you get when doing concurrency through shared memory such as race conditions when multiple processes/threads access the same data in-memory.
Each Durable Object instance can be seen as an Actor instance, receiving messages (incoming HTTP/RPC requests), executing some logic in its own single-threaded context using its attached durable storage or in-memory state, and finally sending messages to the outside world (outgoing HTTP/RPC requests or responses), even to another Durable Object instance.
Each Durable Object has certain capabilities in terms of how much work it can do, which should influence the application's architecture to fully take advantage of the platform.
Durable Objects are natively integrated into Cloudflare's infrastructure, giving you the ultimate serverless platform to build distributed stateful applications exploiting the entirety of Cloudflare's network.
Durable Objects in Cloudflare
Many of Cloudflare's products use Durable Objects. Some of our technical blog posts showcase real-world applications and use-cases where Durable Objects make building applications easier and simpler.
These blog posts may also serve as inspiration on how to architect scalable applications using Durable Objects, and how to integrate them with the rest of Cloudflare Developer Platform.
- Durable Objects aren't just durable, they're fast: a 10x speedup for Cloudflare Queues
- Behind the scenes with Stream Live, Cloudflare's live streaming service
- DO it again: how we used Durable Objects to add WebSockets support and authentication to AI Gateway
- Workers Builds: integrated CI/CD built on the Workers platform
- Build durable applications on Cloudflare Workers: you write the Workflows, we take care of the rest
- Building D1: a Global Database
- Billions and billions (of logs): scaling AI Gateway with the Cloudflare Developer Platform
- Indexing millions of HTTP requests using Durable Objects
Finally, the following blog posts may help you learn some of the technical implementation aspects of Durable Objects, and how they work.
- Durable Objects: Easy, Fast, Correct — Choose three
- Zero-latency SQLite storage in every Durable Object
- Workers Durable Objects Beta: A New Approach to Stateful Serverless
Get started
Get started now by following the "Get started" guide to create your first application using Durable Objects.
[^1]: Storage per Durable Object with SQLite is currently 1 GB. This will be raised to 10 GB for general availability.
Durable Objects code updates
import { WranglerConfig, PackageManagers } from "~/components";
By default, Durable Objects defer code updates for up to 300 seconds (5 minutes) to give in-flight requests time to finish. An active object can keep running its current code until it hibernates or reaches this delay. Deferral is best effort and does not guarantee that every operation finishes.
Deploy a Worker with --durable-objects-code-update-mode immediate to apply a code update right away instead of waiting. For more information on when an object hibernates, refer to Lifecycle of a Durable Object.
Code update strategy
| Mode | Default max_delay |
Behavior |
|---|---|---|
deferred |
300 seconds (5 minutes) | An active Durable Object can continue running its current code until it hibernates. If it is still active when max_delay is reached (86400 seconds maximum), the object resets and adopts the new code. |
immediate |
Not applicable | An active Durable Object resets and adopts the new code immediately, without waiting to hibernate. |
deferred with a 300-second max_delay is the default code update strategy. No configuration is required to use this default.
An explicit code_update_strategy — whether in your Wrangler configuration, passed as a CLI flag, or sent through the REST API — always overrides the default.
How it works
- You deploy a new version to 100% of traffic.
- A client makes a request to a Durable Object that is not running. This uses the latest version of your code.
- If a Durable Object is active with a
deferredcode update strategy, it can keep running its existing version and accepting new requests while it waits to hibernate. If the object hibernates beforemax_delayis reached, it starts on the new version on the next request. - If the object is still active when
max_delayis reached, it resets and adopts the new code.
flowchart TD
A["You deploy a new version"] --> B{"Is the Durable Object<br/>currently running?"}
B -->|Inactive| C["Starts on the new version<br/>on the next request"]
B -->|Active| D{"code_update_strategy<br/>mode?"}
D -->|immediate| E["The object resets and<br/>adopts the new code"]
D -->|deferred| F["Starts a max_delay timer,<br/>keeps running its current version"]
F --> G{"Hibernates before<br/>max_delay timer ends?"}
G -->|Yes| H["Starts on new version<br/>on next request"]
G -->|"No"| E
For each active Durable Object, consecutive Worker deployments have the following behavior:
- Each deployment starts a separate
max_delaytimer for that object. - When the first timer expires, the object resets and adopts the latest deployed version, not necessarily the version that started that timer.
For example, consider an active Durable Object. You deploy version A with max_delay: 30, starting a 30-second timer for that object. Ten seconds later, you deploy version B with max_delay: 60, starting a separate 60-second timer for the same object. Version A's timer expires first, at the 30-second mark. When it expires, the object resets and adopts version B, the latest deployed version. If the object hibernates before either timer expires, it starts on the latest deployed version on the next request.
flowchart LR
A["Deploy version A<br/>max_delay: 30s"] --> B["Timer A:<br/>expires in 30s"]
C["Deploy version B<br/>10s later<br/>max_delay: 60s"] --> D["Timer B:<br/>expires in 60s"]
B --> E["Timer A expires first"]
D -.->|"superseded, never expires"| E
E --> F["The object resets and<br/>adopts version B,<br/>the latest deployed version"]
This means an immediate deploy always forces the update right away, even while an earlier deferred deploy's timer is still running, and it might skip versions you deployed in between.
Configure a code update strategy
Set code_update_strategy for a deployment through your Wrangler configuration, the CLI flag, or the REST API. Wrangler supplies the configured strategy each time you deploy. The strategy does not carry over between deployments: a later deployment that omits it uses the default strategy.
code_update_strategy applies to every Durable Object class this Worker exports — you cannot set a different strategy for one class than another in the same Worker. This includes classes exported through ctx.exports without an explicit bindings entry.
If a Durable Objects binding uses script_name to reference a class defined in a different Worker, that class's code updates are controlled by that other Worker's own code_update_strategy, not the Worker with the binding.
Options
code_update_strategy |
Result |
|---|---|
| (omitted) | Defaults to deferred with a 300-second max_delay. |
{ "mode": "deferred" } |
Uses the default max_delay of 300 seconds (5 minutes). |
{ "mode": "deferred", "max_delay": 0 } |
Applies the update immediately. |
{ "mode": "deferred", "max_delay": 120 } |
The code update is deferred for up to 120 seconds to give the object time to hibernate. |
{ "mode": "immediate" } |
The object adopts the new code immediately, without waiting to hibernate. |
{ "mode": "immediate", "max_delay": 60 } |
The REST API returns an error. Wrangler silently drops max_delay; the object adopts the new code immediately. |
Wrangler
Add code_update_strategy alongside bindings in the durable_objects object of your Wrangler configuration file:
{
"durable_objects": {
"bindings": [
{
"name": "CHAT_ROOM",
"class_name": "ChatRoom",
},
],
"code_update_strategy": {
"mode": "deferred",
"max_delay": 60,
},
},
}
max_delay is in seconds and only applies when mode is deferred. It accepts values from 0 to 86400 (24 hours), with millisecond precision, such as 45.678. Omit max_delay to use the default of 300 seconds (5 minutes).
If you're using ctx.exports and don't need an explicit bindings entry, code_update_strategy still applies on its own:
{
"durable_objects": {
"code_update_strategy": {
"mode": "deferred",
"max_delay": 60,
},
},
}
REST
Add code_update_strategy to a Create deployment request. This field belongs to the deployment, not the Worker version:
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME/deployments" \
--request POST \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"strategy": "percentage",
"versions": [
{
"version_id": "f3b5b2d2-75d6-4c3f-ae3d-4df2f9cfeb7a",
"percentage": 100
}
],
"code_update_strategy": {
"mode": "deferred",
"max_delay": 60
}
}'
code_update_strategy is write-only. Create, get, and list deployment responses do not include it.
Set strategy for a single deployment
Pass --durable-objects-code-update-mode to wrangler deploy, wrangler versions deploy, or wrangler rollback to replace the entire configured strategy for a single deployment. The flag accepts immediate or deferred <duration>. A duration is required with deferred:
Wrangler selects the entire code update strategy in this order:
- The
--durable-objects-code-update-modeflag, including its duration whendeferred. durable_objects.code_update_strategyin your Wrangler configuration.- The default:
{ "mode": "deferred", "max_delay": 300 }.
For a deferred strategy in your Wrangler configuration, omitting max_delay uses 300 seconds. Setting max_delay to 0 applies the update immediately. Flag values do not inherit max_delay from the configuration file.
A single-deployment override is most useful when waiting is worse than resetting. For example, set code_update_strategy to { "mode": "immediate" } (or pass --durable-objects-code-update-mode immediate) when:
- You need to ship a correctness or security fix immediately, even if it interrupts active objects.
- Some of your objects rarely or never hibernate, and you need to force them onto new code, or reduce their duration costs, without waiting.
Gradual deployments
During a gradual deployment, Cloudflare assigns each Durable Object to one of the versions in the deployment. Increasing a version's traffic percentage assigns more objects to that version. Only the newly assigned objects follow the mechanism described in How it works above — for example, increasing a version's traffic percentage from 20% to 50% starts a max_delay wait only for the additional 30% of objects newly assigned to it. Objects that already run that version are not affected.
Keep your Worker and Durable Object code forward and backward compatible for as long as any version might still be running, not just for the length of the deployment. Refer to Code updates.
Limitations
- A
deferredcode update strategy is best effort. Cloudflare does not guarantee that every operation finishes or that an object keeps running its current code for the entiremax_delay. - A code update strategy does not delay restarts unrelated to code updates, such as Workers runtime updates.
Related resources
- Lifecycle of a Durable Object
- Gradual deployments with Durable Objects
- Code updates
- Troubleshoot Durable Object resets
Deployments
import { DirectoryListing } from "~/components";
Use the Alarms API
import { GlossaryTooltip, TabItem, Tabs, WranglerConfig } from "~/components";
This example implements an alarm() handler that allows batching of requests to a single Durable Object.
When a request is received and no alarm is set, it sets an alarm for 10 seconds in the future. The alarm() handler processes all requests received within that 10-second window.
If no new requests are received, no further alarms will be set until the next request arrives.
import { DurableObject } from "cloudflare:workers";
// Worker
export default {
async fetch(request, env) {
return await env.BATCHER.getByName("foo").fetch(request);
},
};
// Durable Object
export class Batcher extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
this.storage = ctx.storage;
this.ctx.blockConcurrencyWhile(async () => {
let vals = await this.storage.list({ reverse: true, limit: 1 });
this.count = vals.size == 0 ? 0 : parseInt(vals.keys().next().value);
});
}
async fetch(request) {
this.count++;
// If there is no alarm currently set, set one for 10 seconds from now
// Any further POSTs in the next 10 seconds will be part of this batch.
let currentAlarm = await this.storage.getAlarm();
if (currentAlarm == null) {
this.storage.setAlarm(Date.now() + 1000 * 10);
}
// Add the request to the batch.
await this.storage.put(this.count, await request.text());
return new Response(JSON.stringify({ queued: this.count }), {
headers: {
"content-type": "application/json;charset=UTF-8",
},
});
}
async alarm() {
let vals = await this.storage.list();
await fetch("http://example.com/some-upstream-service", {
method: "POST",
body: Array.from(vals.values()),
});
await this.storage.deleteAll();
this.count = 0;
}
}
from workers import DurableObject, Response, WorkerEntrypoint, fetch
import time
# Worker
class Default(WorkerEntrypoint):
async def fetch(self, request):
stub = self.env.BATCHER.getByName("foo")
return await stub.fetch(request)
# Durable Object
class Batcher(DurableObject):
def __init__(self, ctx, env):
super().__init__(ctx, env)
self.storage = ctx.storage
@self.ctx.blockConcurrencyWhile
async def initialize():
vals = await self.storage.list(reverse=True, limit=1)
self.count = 0
if len(vals) > 0:
self.count = int(vals.keys().next().value)
async def fetch(self, request):
self.count += 1
# If there is no alarm currently set, set one for 10 seconds from now
# Any further POSTs in the next 10 seconds will be part of this batch.
current_alarm = await self.storage.getAlarm()
if current_alarm is None:
self.storage.setAlarm(int(time.time() * 1000) + 1000 * 10)
# Add the request to the batch.
await self.storage.put(self.count, await request.text())
return Response.json(
{"queued": self.count}
)
async def alarm(self):
vals = await self.storage.list()
await fetch(
"http://example.com/some-upstream-service",
method="POST",
body=list(vals.values())
)
await self.storage.deleteAll()
self.count = 0
The alarm() handler will be called once every 10 seconds. If an unexpected error terminates the Durable Object, the alarm() handler will be re-instantiated on another machine. Following a short delay, the alarm() handler will run from the beginning on the other machine.
Finally, configure your Wrangler file to include a Durable Object binding and migration based on the namespace and class name chosen previously.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "durable-object-alarm",
"main": "src/index.ts",
"upload_source_maps": true,
"durable_objects": {
"bindings": [
{
"name": "BATCHER",
"class_name": "Batcher"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"Batcher"
]
}
]
}
Build a counter
import { TabItem, Tabs, WranglerConfig } from "~/components";
This example shows how to build a counter using Durable Objects and Workers with RPC methods that can print, increment, and decrement a name provided by the URL query string parameter, for example, ?name=A.
import { DurableObject } from "cloudflare:workers";
// Worker
export default {
async fetch(request, env) {
let url = new URL(request.url);
let name = url.searchParams.get("name");
if (!name) {
return new Response(
"Select a Durable Object to contact by using" +
" the `name` URL query string parameter, for example, ?name=A",
);
}
// A stub is a client Object used to send messages to the Durable Object.
let stub = env.COUNTERS.getByName(name);
// Send a request to the Durable Object using RPC methods, then await its response.
let count = null;
switch (url.pathname) {
case "/increment":
count = await stub.increment();
break;
case "/decrement":
count = await stub.decrement();
break;
case "/":
// Serves the current value.
count = await stub.getCounterValue();
break;
default:
return new Response("Not found", { status: 404 });
}
return new Response(`Durable Object '${name}' count: ${count}`);
},
};
// Durable Object
export class Counter extends DurableObject {
async getCounterValue() {
let value = (await this.ctx.storage.get("value")) || 0;
return value;
}
async increment(amount = 1) {
let value = (await this.ctx.storage.get("value")) || 0;
value += amount;
// You do not have to worry about a concurrent request having modified the value in storage.
// "input gates" will automatically protect against unwanted concurrency.
// Read-modify-write is safe.
await this.ctx.storage.put("value", value);
return value;
}
async decrement(amount = 1) {
let value = (await this.ctx.storage.get("value")) || 0;
value -= amount;
await this.ctx.storage.put("value", value);
return value;
}
}
import { DurableObject } from "cloudflare:workers";
export interface Env {
COUNTERS: DurableObjectNamespace<Counter>;
}
// Worker
export default {
async fetch(request, env) {
let url = new URL(request.url);
let name = url.searchParams.get("name");
if (!name) {
return new Response(
"Select a Durable Object to contact by using" +
" the `name` URL query string parameter, for example, ?name=A",
);
}
// A stub is a client Object used to send messages to the Durable Object.
let stub = env.COUNTERS.get(name);
let count = null;
switch (url.pathname) {
case "/increment":
count = await stub.increment();
break;
case "/decrement":
count = await stub.decrement();
break;
case "/":
// Serves the current value.
count = await stub.getCounterValue();
break;
default:
return new Response("Not found", { status: 404 });
}
return new Response(`Durable Object '${name}' count: ${count}`);
},
} satisfies ExportedHandler<Env>;
// Durable Object
export class Counter extends DurableObject {
async getCounterValue() {
let value = (await this.ctx.storage.get("value")) || 0;
return value;
}
async increment(amount = 1) {
let value: number = (await this.ctx.storage.get("value")) || 0;
value += amount;
// You do not have to worry about a concurrent request having modified the value in storage.
// "input gates" will automatically protect against unwanted concurrency.
// Read-modify-write is safe.
await this.ctx.storage.put("value", value);
return value;
}
async decrement(amount = 1) {
let value: number = (await this.ctx.storage.get("value")) || 0;
value -= amount;
await this.ctx.storage.put("value", value);
return value;
}
}
from workers import DurableObject, Response, WorkerEntrypoint
from urllib.parse import urlparse, parse_qs
# Worker
class Default(WorkerEntrypoint):
async def fetch(self, request):
parsed_url = urlparse(request.url)
query_params = parse_qs(parsed_url.query)
name = query_params.get('name', [None])[0]
if not name:
return Response(
"Select a Durable Object to contact by using"
+ " the `name` URL query string parameter, for example, ?name=A"
)
# A stub is a client Object used to send messages to the Durable Object.
stub = self.env.COUNTERS.getByName(name)
# Send a request to the Durable Object using RPC methods, then await its response.
count = None
if parsed_url.path == "/increment":
count = await stub.increment()
elif parsed_url.path == "/decrement":
count = await stub.decrement()
elif parsed_url.path == "" or parsed_url.path == "/":
# Serves the current value.
count = await stub.getCounterValue()
else:
return Response("Not found", status=404)
return Response(f"Durable Object '{name}' count: {count}")
# Durable Object
class Counter(DurableObject):
def __init__(self, ctx, env):
super().__init__(ctx, env)
async def getCounterValue(self):
value = await self.ctx.storage.get("value")
return value if value is not None else 0
async def increment(self, amount=1):
value = await self.ctx.storage.get("value")
value = (value if value is not None else 0) + amount
# You do not have to worry about a concurrent request having modified the value in storage.
# "input gates" will automatically protect against unwanted concurrency.
# Read-modify-write is safe.
await self.ctx.storage.put("value", value)
return value
async def decrement(self, amount=1):
value = await self.ctx.storage.get("value")
value = (value if value is not None else 0) - amount
await self.ctx.storage.put("value", value)
return value
Finally, configure your Wrangler file to include a Durable Object binding and migration based on the namespace and class name chosen previously.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-counter",
"main": "src/index.ts",
"upload_source_maps": true,
"durable_objects": {
"bindings": [
{
"name": "COUNTERS",
"class_name": "Counter"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"Counter"
]
}
]
}
Related resources
Durable Object in-memory state
import { TabItem, Tabs, WranglerConfig } from "~/components";
This example shows you how Durable Objects are stateful, meaning in-memory state can be retained between requests. After a brief period of inactivity, the Durable Object will be evicted, and all in-memory state will be lost. The next request will reconstruct the object, but instead of showing the city of the previous request, it will display a message indicating that the object has been reinitialized. If you need your applications state to survive eviction, write the state to storage by using the Storage API, or by storing your data elsewhere.
import { DurableObject } from "cloudflare:workers";
// Worker
export default {
async fetch(request, env) {
return await handleRequest(request, env);
},
};
async function handleRequest(request, env) {
let stub = env.LOCATION.getByName("A");
// Forward the request to the remote Durable Object.
let resp = await stub.fetch(request);
// Return the response to the client.
return new Response(await resp.text());
}
// Durable Object
export class Location extends DurableObject {
constructor(state, env) {
super(state, env);
// Upon construction, you do not have a location to provide.
// This value will be updated as people access the Durable Object.
// When the Durable Object is evicted from memory, this will be reset.
this.location = null;
}
// Handle HTTP requests from clients.
async fetch(request) {
let response = null;
if (this.location == null) {
response = new String(`
This is the first request, you called the constructor, so this.location was null.
You will set this.location to be your city: (${request.cf.city}). Try reloading the page.`);
} else {
response = new String(`
The Durable Object was already loaded and running because it recently handled a request.
Previous Location: ${this.location}
New Location: ${request.cf.city}`);
}
// You set the new location to be the new city.
this.location = request.cf.city;
console.log(response);
return new Response(response);
}
}
from workers import DurableObject, Response, WorkerEntrypoint
# Worker
class Default(WorkerEntrypoint):
async def fetch(self, request):
return await handle_request(request, self.env)
async def handle_request(request, env):
stub = env.LOCATION.getByName("A")
# Forward the request to the remote Durable Object.
resp = await stub.fetch(request)
# Return the response to the client.
return Response(await resp.text())
# Durable Object
class Location(DurableObject):
def __init__(self, ctx, env):
super().__init__(ctx, env)
# Upon construction, you do not have a location to provide.
# This value will be updated as people access the Durable Object.
# When the Durable Object is evicted from memory, this will be reset.
self.location = None
# Handle HTTP requests from clients.
async def fetch(self, request):
response = None
if self.location is None:
response = f"""
This is the first request, you called the constructor, so this.location was null.
You will set this.location to be your city: ({request.js_object.cf.city}). Try reloading the page."""
else:
response = f"""
The Durable Object was already loaded and running because it recently handled a request.
Previous Location: {self.location}
New Location: {request.js_object.cf.city}"""
# You set the new location to be the new city.
self.location = request.js_object.cf.city
print(response)
return Response(response)
Finally, configure your Wrangler file to include a Durable Object binding and migration based on the namespace and class name chosen previously.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "durable-object-in-memory-state",
"main": "src/index.ts",
"upload_source_maps": true,
"durable_objects": {
"bindings": [
{
"name": "LOCATION",
"class_name": "Location"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"Location"
]
}
]
}
Durable Object Time To Live
import { TabItem, Tabs, GlossaryTooltip, WranglerConfig } from "~/components";
A common feature request for Durable Objects is a Time To Live (TTL) for Durable Object instances. Durable Objects give developers the tools to implement a custom TTL in only a few lines of code. This example demonstrates how to implement a TTL making use of alarms. While this TTL will be extended upon every new request to the Durable Object, this can be customized based on a particular use case.
:::caution[Be careful when calling setAlarm in the Durable Object class constructor]
In this example the TTL is extended upon every new fetch request to the Durable Object. It might be tempting to instead extend the TTL in the constructor of the Durable Object. This is not advised because the Durable Object's constructor will be called before invoking the alarm handler if the alarm wakes the Durable Object up from hibernation. This approach will naively result in the constructor continually extending the TTL without running the alarm handler. If you must call setAlarm in the Durable Object class constructor be sure to check that there is no alarm previously set.
:::
import { DurableObject } from "cloudflare:workers";
// Durable Object
export class MyDurableObject extends DurableObject {
// Time To Live (TTL) in milliseconds
timeToLiveMs = 1000;
constructor(ctx, env) {
super(ctx, env);
}
async fetch(_request) {
// Extend the TTL immediately following every fetch request to a Durable Object.
await this.ctx.storage.setAlarm(Date.now() + this.timeToLiveMs);
...
}
async alarm() {
await this.ctx.storage.deleteAll();
}
}
// Worker
export default {
async fetch(request, env) {
const stub = env.MY_DURABLE_OBJECT.getByName("foo");
return await stub.fetch(request);
},
};
import { DurableObject } from "cloudflare:workers";
export interface Env {
MY_DURABLE_OBJECT: DurableObjectNamespace<MyDurableObject>;
}
// Durable Object
export class MyDurableObject extends DurableObject {
// Time To Live (TTL) in milliseconds
timeToLiveMs = 1000;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}
async fetch(_request: Request) {
// Extend the TTL immediately following every fetch request to a Durable Object.
await this.ctx.storage.setAlarm(Date.now() + this.timeToLiveMs);
...
}
async alarm() {
await this.ctx.storage.deleteAll();
}
}
// Worker
export default {
async fetch(request, env) {
const stub = env.MY_DURABLE_OBJECT.getByName("foo");
return await stub.fetch(request);
},
} satisfies ExportedHandler<Env>;
from workers import DurableObject, Response, WorkerEntrypoint
import time
# Durable Object
class MyDurableObject(DurableObject):
# Time To Live (TTL) in milliseconds
timeToLiveMs = 1000
def __init__(self, ctx, env):
super().__init__(ctx, env)
async def fetch(self, _request):
# Extend the TTL immediately following every fetch request to a Durable Object.
await self.ctx.storage.setAlarm(int(time.time() * 1000) + self.timeToLiveMs)
...
async def alarm(self):
await self.ctx.storage.deleteAll()
# Worker
class Default(WorkerEntrypoint):
async def fetch(self, request):
stub = self.env.MY_DURABLE_OBJECT.getByName("foo")
return await stub.fetch(request)
To test and deploy this example, configure your Wrangler file to include a Durable Object binding and migration based on the namespace and class name chosen previously.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "durable-object-ttl",
"main": "src/index.ts",
"upload_source_maps": true,
"durable_objects": {
"bindings": [
{
"name": "MY_DURABLE_OBJECT",
"class_name": "MyDurableObject"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"MyDurableObject"
]
}
]
}
Examples
import { ResourcesBySelector, GlossaryTooltip } from "~/components";
Explore the following examples for Durable Objects.
<ResourcesBySelector types={["example"]} directory="durable-objects/examples/" />
Use ReadableStream with Durable Object and Workers
import { GlossaryTooltip, WranglerConfig, TypeScriptExample, } from "~/components";
This example demonstrates:
- A Worker receives a request, and forwards it to a Durable Object
my-id. - The Durable Object streams an incrementing number every second, until it receives
AbortSignal. - The Worker reads and logs the values from the stream.
- The Worker then cancels the stream after 5 values.
// Send incremented counter value every second async function* dataSource(signal: AbortSignal) { let counter = 0; while (!signal.aborted) { yield counter++; await new Promise((resolve) => setTimeout(resolve, 1_000)); }
console.log('Data source cancelled');
}
export class MyDurableObject extends DurableObject { async fetch(request: Request): Promise { const abortController = new AbortController();
const stream = new ReadableStream({
async start(controller) {
if (request.signal.aborted) {
controller.close();
abortController.abort();
return;
}
for await (const value of dataSource(abortController.signal)) {
controller.enqueue(new TextEncoder().encode(String(value)));
}
},
cancel() {
console.log('Stream cancelled');
abortController.abort();
},
});
const headers = new Headers({
'Content-Type': 'application/octet-stream',
});
return new Response(stream, { headers });
}
}
export default { async fetch(request, env, ctx): Promise { const stub = env.MY_DURABLE_OBJECT.getByName("foo"); const response = await stub.fetch(request, { ...request }); if (!response.ok || !response.body) { return new Response('Invalid response', { status: 500 }); }
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let data = [] as string[];
let i = 0;
while (true) {
// Cancel the stream after 5 messages
if (i > 5) {
reader.cancel();
break;
}
const { value, done } = await reader.read();
if (value) {
console.log(`Got value ${value}`);
data = [...data, value];
}
if (done) {
break;
}
i++;
}
return Response.json(data);
},
} satisfies ExportedHandler;
</TypeScriptExample>
:::note
In a setup where a Durable Object returns a readable stream to a Worker, if the Worker cancels the Durable Object's readable stream, the cancellation propagates to the Durable Object.
:::
Use RpcTarget class to handle Durable Object metadata
import { TabItem, Tabs, GlossaryTooltip } from "~/components";
When working with Durable Objects, you will often need the name that was used to create the Durable Object via idFromName(). This name is typically a meaningful identifier that represents what the Durable Object is responsible for (like a user ID, room name, or resource identifier).
:::note
If the caller accesses the Durable Object with getByName() or idFromName(), you can read the name directly inside the Durable Object from this.ctx.id.name. You do not need the pattern on this page for that. Refer to Durable Object ID for details, including when ctx.id.name is undefined.
:::
The RpcTarget pattern in the following examples is useful when you need to pass metadata that is not available on ctx.id, or when ctx.id.name is undefined (for example, when the caller uses idFromString()). It creates a communication layer that automatically carries the metadata with each method call, which keeps your API clean.
Based on your needs, you can either store the metadata temporarily in the RpcTarget class, or use Durable Object storage to persist the metadata for the lifetime of the object.
This example does not persist the Durable Object metadata. It demonstrates how to:
- Create an
RpcTargetclass - Set the Durable Object metadata (identifier in this example) in the
RpcTargetclass - Pass the metadata to a Durable Object method
- Clean up the
RpcTargetclass after use
import { DurableObject, RpcTarget } from "cloudflare:workers";
// * Create an RpcDO class that extends RpcTarget
// * Use this class to set the Durable Object metadata
// * Pass the metadata in the Durable Object methods
// * @param mainDo - The main Durable Object class
// * @param doIdentifier - The identifier of the Durable Object
export class RpcDO extends RpcTarget {
constructor(
private mainDo: MyDurableObject,
private doIdentifier: string,
) {
super();
}
// * Pass the user's name to the Durable Object method
// * @param userName - The user's name to pass to the Durable Object method
async computeMessage(userName: string): Promise<string> {
// Call the Durable Object method and pass the user's name and the Durable Object identifier
return this.mainDo.computeMessage(userName, this.doIdentifier);
}
// * Call the Durable Object method without using the Durable Object identifier
// * @param userName - The user's name to pass to the Durable Object method
async simpleGreeting(userName: string) {
return this.mainDo.simpleGreeting(userName);
}
}
// * Create a Durable Object class
// * You can use the RpcDO class to set the Durable Object metadata
export class MyDurableObject extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}
// * Initialize the RpcDO class
// * You can set the Durable Object metadata here
// * It returns an instance of the RpcDO class
// * @param doIdentifier - The identifier of the Durable Object
async setMetaData(doIdentifier: string) {
return new RpcDO(this, doIdentifier);
}
// * Function that computes a greeting message using the user's name and DO identifier
// * @param userName - The user's name to include in the greeting
// * @param doIdentifier - The identifier of the Durable Object
async computeMessage(
userName: string,
doIdentifier: string,
): Promise<string> {
console.log({
userName: userName,
durableObjectIdentifier: doIdentifier,
});
return `Hello, ${userName}! The identifier of this DO is ${doIdentifier}`;
}
// * Function that is not in the RpcTarget
// * Not every function has to be in the RpcTarget
private async notInRpcTarget() {
return "This is not in the RpcTarget";
}
// * Function that takes the user's name and does not use the Durable Object identifier
// * @param userName - The user's name to include in the greeting
async simpleGreeting(userName: string) {
// Call the private function that is not in the RpcTarget
console.log(this.notInRpcTarget());
return `Hello, ${userName}! This doesn't use the DO identifier.`;
}
}
export default {
async fetch(request, env, ctx): Promise<Response> {
let id: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName(
new URL(request.url).pathname,
);
let stub = env.MY_DURABLE_OBJECT.get(id);
// * Set the Durable Object metadata using the RpcTarget
// * Notice that no await is needed here
const rpcTarget = stub.setMetaData(id.name ?? "default");
// Call the Durable Object method using the RpcTarget.
// The DO identifier is passed in the RpcTarget
const greeting = await rpcTarget.computeMessage("world");
// Call the Durable Object method that does not use the Durable Object identifier
const simpleGreeting = await rpcTarget.simpleGreeting("world");
// Clean up the RpcTarget.
try {
(await rpcTarget)[Symbol.dispose]?.();
console.log("RpcTarget cleaned up.");
} catch (e) {
console.error({
message: "RpcTarget could not be cleaned up.",
error: String(e),
errorProperties: e,
});
}
return new Response(greeting, { status: 200 });
},
} satisfies ExportedHandler<Env>;
This example persists the Durable Object metadata. It demonstrates similar steps as the previous example, but uses Durable Object storage to store the identifier, eliminating the need to pass it through the RpcTarget.
import { DurableObject, RpcTarget } from "cloudflare:workers";
// * Create an RpcDO class that extends RpcTarget
// * Use this class to set the Durable Object metadata
// * Pass the metadata in the Durable Object methods
// * @param mainDo - The main Durable Object class
// * @param doIdentifier - The identifier of the Durable Object
export class RpcDO extends RpcTarget {
constructor(
private mainDo: MyDurableObject,
private doIdentifier: string,
) {
super();
}
// * Pass the user's name to the Durable Object method
// * @param userName - The user's name to pass to the Durable Object method
async computeMessage(userName: string): Promise<string> {
// Call the Durable Object method and pass the user's name and the Durable Object identifier
return this.mainDo.computeMessage(userName, this.doIdentifier);
}
// * Call the Durable Object method without using the Durable Object identifier
// * @param userName - The user's name to pass to the Durable Object method
async simpleGreeting(userName: string) {
return this.mainDo.simpleGreeting(userName);
}
}
// * Create a Durable Object class
// * You can use the RpcDO class to set the Durable Object metadata
export class MyDurableObject extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}
// * Initialize the RpcDO class
// * You can set the Durable Object metadata here
// * It returns an instance of the RpcDO class
// * @param doIdentifier - The identifier of the Durable Object
async setMetaData(doIdentifier: string) {
// Use DO storage to store the Durable Object identifier
await this.ctx.storage.put("doIdentifier", doIdentifier);
return new RpcDO(this, doIdentifier);
}
// * Function that computes a greeting message using the user's name and DO identifier
// * @param userName - The user's name to include in the greeting
async computeMessage(userName: string): Promise<string> {
// Get the DO identifier from storage
const doIdentifier = await this.ctx.storage.get("doIdentifier");
console.log({
userName: userName,
durableObjectIdentifier: doIdentifier,
});
return `Hello, ${userName}! The identifier of this DO is ${doIdentifier}`;
}
// * Function that is not in the RpcTarget
// * Not every function has to be in the RpcTarget
private async notInRpcTarget() {
return "This is not in the RpcTarget";
}
// * Function that takes the user's name and does not use the Durable Object identifier
// * @param userName - The user's name to include in the greeting
async simpleGreeting(userName: string) {
// Call the private function that is not in the RpcTarget
console.log(this.notInRpcTarget());
return `Hello, ${userName}! This doesn't use the DO identifier.`;
}
}
export default {
async fetch(request, env, ctx): Promise<Response> {
let id: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName(
new URL(request.url).pathname,
);
let stub = env.MY_DURABLE_OBJECT.get(id);
// * Set the Durable Object metadata using the RpcTarget
// * Notice that no await is needed here
const rpcTarget = stub.setMetaData(id.name ?? "default");
// Call the Durable Object method using the RpcTarget.
// The DO identifier is stored in the Durable Object's storage
const greeting = await rpcTarget.computeMessage("world");
// Call the Durable Object method that does not use the Durable Object identifier
const simpleGreeting = await rpcTarget.simpleGreeting("world");
// Clean up the RpcTarget.
try {
(await rpcTarget)[Symbol.dispose]?.();
console.log("RpcTarget cleaned up.");
} catch (e) {
console.error({
message: "RpcTarget could not be cleaned up.",
error: String(e),
errorProperties: e,
});
}
return new Response(greeting, { status: 200 });
},
} satisfies ExportedHandler<Env>;
Testing Durable Objects
import { WranglerConfig, TypeScriptExample, Tabs, TabItem } from "~/components";
Use the @cloudflare/vitest-plugin package to write tests for your Durable Objects. This integration runs your tests inside the Workers runtime, giving you direct access to Durable Object bindings and APIs.
Prerequisites
Install Vitest and the Workers Vitest integration as dev dependencies:
```sh npm i -D vitest@^4.1.0 @cloudflare/vitest-plugin ``` ```sh pnpm add -D vitest@^4.1.0 @cloudflare/vitest-plugin ``` ```sh yarn add -D vitest@^4.1.0 @cloudflare/vitest-plugin ```Example Durable Object
This example tests a simple counter Durable Object with SQLite storage:
```ts import { DurableObject } from "cloudflare:workers";export interface Env { COUNTER: DurableObjectNamespace; }
export class Counter extends DurableObject { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS counters (
name TEXT PRIMARY KEY,
value INTEGER NOT NULL DEFAULT 0
)
`);
});
}
// In-memory only. This field lives on the instance and is not persisted
// to storage, so it is reset whenever the Durable Object is evicted and
// reconstructed.
cachedHits = 0;
recordHit(): number {
return ++this.cachedHits;
}
getHits(): number {
return this.cachedHits;
}
async increment(name: string = "default"): Promise<number> {
this.ctx.storage.sql.exec(
`INSERT INTO counters (name, value) VALUES (?, 1)
ON CONFLICT(name) DO UPDATE SET value = value + 1`,
name
);
const result = this.ctx.storage.sql
.exec<{ value: number }>("SELECT value FROM counters WHERE name = ?", name)
.one();
return result.value;
}
async getCount(name: string = "default"): Promise<number> {
const result = this.ctx.storage.sql
.exec<{ value: number }>("SELECT value FROM counters WHERE name = ?", name)
.toArray();
return result[0]?.value ?? 0;
}
async reset(name: string = "default"): Promise<void> {
this.ctx.storage.sql.exec("DELETE FROM counters WHERE name = ?", name);
}
}
export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); const counterId = url.searchParams.get("id") ?? "default";
const id = env.COUNTER.idFromName(counterId);
const stub = env.COUNTER.get(id);
if (request.method === "POST") {
const count = await stub.increment();
return Response.json({ count });
}
const count = await stub.getCount();
return Response.json({ count });
},
};
</TypeScriptExample>
## Configure Vitest
Create a `vitest.config.ts` file that uses the `cloudflareTest()` plugin:
```ts title="vitest.config.ts"
import { cloudflareTest } from "@cloudflare/vitest-plugin";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [
cloudflareTest({
wrangler: { configPath: "./wrangler.jsonc" },
}),
],
});
Make sure your Wrangler configuration includes the Durable Object binding and SQLite migration:
```jsonc { "name": "counter-worker", "main": "src/index.ts", "upload_source_maps": true, "compatibility_date": "$today", "durable_objects": { "bindings": [ { "name": "COUNTER", "class_name": "Counter" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["Counter"] } ] } ```Define types for tests
Create a test/tsconfig.json to configure TypeScript for your tests:
{
"extends": "../tsconfig.json",
"compilerOptions": {
"moduleResolution": "bundler",
"types": ["@cloudflare/vitest-plugin/types"]
},
"include": ["./**/*.ts", "../src/worker-configuration.d.ts"]
}
Create an env.d.ts file to type the test environment:
declare module "cloudflare:workers" {
interface ProvidedEnv extends Env {}
}
Writing tests
Unit tests with direct Durable Object access
You can get a stub to a Durable Object directly from the env object provided by cloudflare:workers:
describe("Counter Durable Object", () => { it("should increment the counter", async () => { const id = env.COUNTER.idFromName("test-counter"); const stub = env.COUNTER.get(id);
// Call RPC methods directly on the stub
const count1 = await stub.increment();
expect(count1).toBe(1);
const count2 = await stub.increment();
expect(count2).toBe(2);
const count3 = await stub.increment();
expect(count3).toBe(3);
});
it("should persist storage within a test file", async () => {
const id = env.COUNTER.idFromName("test-counter");
const stub = env.COUNTER.get(id);
expect(await stub.getCount()).toBe(3);
});
it("should reset a counter", async () => {
const id = env.COUNTER.idFromName("test-counter");
const stub = env.COUNTER.get(id);
await stub.increment("my-counter");
await stub.increment("my-counter");
expect(await stub.getCount("my-counter")).toBe(2);
await stub.reset("my-counter");
expect(await stub.getCount("my-counter")).toBe(0);
});
it("should isolate different Durable Object instances", async () => {
const id1 = env.COUNTER.idFromName("counter-1");
const id2 = env.COUNTER.idFromName("counter-2");
const stub1 = env.COUNTER.get(id1);
const stub2 = env.COUNTER.get(id2);
await stub1.increment();
await stub1.increment();
await stub2.increment();
// Each Durable Object instance has its own storage
expect(await stub1.getCount()).toBe(2);
expect(await stub2.getCount()).toBe(1);
});
});
</TypeScriptExample>
### Integration tests with `exports`
Use `exports.default.fetch()` to test your Worker's HTTP handler, which routes requests to Durable Objects:
<TypeScriptExample filename="test/integration.test.ts">
```ts
import { exports } from "cloudflare:workers";
import { describe, it, expect } from "vitest";
describe("Counter Worker integration", () => {
it("should increment via HTTP POST", async () => {
const response = await exports.default.fetch("http://example.com?id=http-test", {
method: "POST",
});
expect(response.status).toBe(200);
const data = await response.json<{ count: number }>();
expect(data.count).toBe(1);
});
it("should get count via HTTP GET", async () => {
// First increment the counter
await exports.default.fetch("http://example.com?id=get-test", { method: "POST" });
await exports.default.fetch("http://example.com?id=get-test", { method: "POST" });
// Then get the count
const response = await exports.default.fetch("http://example.com?id=get-test");
const data = await response.json<{ count: number }>();
expect(data.count).toBe(2);
});
it("should use different counters for different IDs", async () => {
await exports.default.fetch("http://example.com?id=counter-a", { method: "POST" });
await exports.default.fetch("http://example.com?id=counter-a", { method: "POST" });
await exports.default.fetch("http://example.com?id=counter-b", { method: "POST" });
const responseA = await exports.default.fetch("http://example.com?id=counter-a");
const responseB = await exports.default.fetch("http://example.com?id=counter-b");
const dataA = await responseA.json<{ count: number }>();
const dataB = await responseB.json<{ count: number }>();
expect(dataA.count).toBe(2);
expect(dataB.count).toBe(1);
});
});
Direct access to Durable Object internals
Use runInDurableObject() to access instance properties and storage directly. This is useful for verifying internal state or testing private methods:
describe("Direct Durable Object access", () => { it("can access instance internals and storage", async () => { const id = env.COUNTER.idFromName("direct-test"); const stub = env.COUNTER.get(id);
// First, interact normally via RPC
await stub.increment();
await stub.increment();
// Then use runInDurableObject to inspect internals
await runInDurableObject(stub, async (instance: Counter, state) => {
// Access the exact same class instance
expect(instance).toBeInstanceOf(Counter);
// Access storage directly for verification
const result = state.storage.sql
.exec<{ value: number }>(
"SELECT value FROM counters WHERE name = ?",
"default"
)
.one();
expect(result.value).toBe(2);
});
});
it("can list all Durable Object IDs in a namespace", async () => {
// Create some Durable Objects
const id1 = env.COUNTER.idFromName("list-test-1");
const id2 = env.COUNTER.idFromName("list-test-2");
await env.COUNTER.get(id1).increment();
await env.COUNTER.get(id2).increment();
// List all IDs in the namespace
const ids = await listDurableObjectIds(env.COUNTER);
expect(ids.length).toBeGreaterThanOrEqual(2);
expect(ids.some((id) => id.equals(id1))).toBe(true);
expect(ids.some((id) => id.equals(id2))).toBe(true);
});
});
</TypeScriptExample>
### Testing SQLite storage
SQLite-backed Durable Objects work seamlessly in tests. The SQL API is available when your Durable Object class is configured with `new_sqlite_classes` in your Wrangler configuration:
<TypeScriptExample filename="test/sqlite.test.ts">
```ts
import { env } from "cloudflare:workers";
import { runInDurableObject } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("SQLite in Durable Objects", () => {
it("can query and verify SQLite storage", async () => {
const id = env.COUNTER.idFromName("sqlite-test");
const stub = env.COUNTER.get(id);
// Increment the counter a few times via RPC
await stub.increment("page-views");
await stub.increment("page-views");
await stub.increment("api-calls");
// Verify the data directly in SQLite
await runInDurableObject(stub, async (instance, state) => {
// Query the database directly
const rows = state.storage.sql
.exec<{ name: string; value: number }>("SELECT name, value FROM counters ORDER BY name")
.toArray();
expect(rows).toEqual([
{ name: "api-calls", value: 1 },
{ name: "page-views", value: 2 },
]);
// Check database size is non-zero
expect(state.storage.sql.databaseSize).toBeGreaterThan(0);
});
});
});
Testing alarms
Use runDurableObjectAlarm() to immediately trigger a scheduled alarm without waiting for the timer. This allows you to test alarm handlers synchronously:
describe("Durable Object alarms", () => { it("can trigger alarms immediately", async () => { const id = env.COUNTER.idFromName("alarm-test"); const stub = env.COUNTER.get(id);
// Increment counter and schedule a reset alarm
await stub.increment();
await stub.increment();
expect(await stub.getCount()).toBe(2);
// Schedule an alarm (in a real app, this might be hours in the future)
await runInDurableObject(stub, async (instance, state) => {
await state.storage.setAlarm(Date.now() + 60_000); // 1 minute from now
});
// Immediately execute the alarm without waiting
const alarmRan = await runDurableObjectAlarm(stub);
expect(alarmRan).toBe(true); // Alarm was scheduled and executed
// Verify the alarm handler ran (assuming it resets the counter)
// Note: You'll need an alarm() method in your Durable Object that handles resets
// expect(await stub.getCount()).toBe(0);
// Trying to run the alarm again returns false (no alarm scheduled)
const alarmRanAgain = await runDurableObjectAlarm(stub);
expect(alarmRanAgain).toBe(false);
});
});
</TypeScriptExample>
To test alarms, add an `alarm()` method to your Durable Object:
<TypeScriptExample filename="src/index.ts">
```ts
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject {
// ... other methods ...
async alarm() {
// This method is called when the alarm fires
// Reset all counters
this.ctx.storage.sql.exec("DELETE FROM counters");
}
async scheduleReset(afterMs: number) {
await this.ctx.storage.setAlarm(Date.now() + afterMs);
}
}
Testing eviction
Use evictDurableObject() to evict a Durable Object instance during tests. Eviction tears down the instance to reset its in-memory state. This lets you test how your Durable Object recovers state from storage after being evicted.
By default, hibernatable WebSockets are hibernated rather than closed, and eviction waits up to 30 seconds for in-flight requests to drain before tearing down the instance.
The following test sets both in-memory state (cachedHits) and durable storage (the counter value), evicts the Durable Object, and verifies that the in-memory state is wiped while the stored count survives:
describe("Durable Object eviction", () => { it("wipes in-memory state but preserves storage across eviction", async () => { const id = env.COUNTER.idFromName("evict-test"); const stub = env.COUNTER.get(id);
// Persist a value to SQLite storage
await stub.increment();
await stub.increment();
expect(await stub.getCount()).toBe(2);
// Set in-memory only state, which is not persisted to storage
await stub.recordHit();
await stub.recordHit();
expect(await stub.getHits()).toBe(2);
// Evict the Durable Object. The in-memory instance is torn down,
// but durable storage is preserved.
await evictDurableObject(stub);
// In-memory state is wiped: the reconstructed instance starts fresh
expect(await stub.getHits()).toBe(0);
// Durable storage survives: the persisted count is read back
expect(await stub.getCount()).toBe(2);
});
});
</TypeScriptExample>
#### Testing WebSocket behavior across eviction
You can control what happens to hibernatable WebSockets when a Durable Object is evicted by passing the `options` parameter:
- `{ webSockets: "hibernate" }` (the default) hibernates WebSockets so they can resume after eviction.
- `{ webSockets: "close" }` closes WebSockets during eviction.
The following example uses a Durable Object that accepts WebSocket connections with the [hibernatable WebSockets API](/durable-objects/best-practices/websockets/):
<TypeScriptExample filename="src/websocket-server.ts">
```ts
import { DurableObject } from "cloudflare:workers";
export class WebSocketServer extends DurableObject<Env> {
async fetch(request: Request): Promise<Response> {
const [client, server] = Object.values(new WebSocketPair());
// Accept the WebSocket as hibernatable so it can survive eviction
this.ctx.acceptWebSocket(server);
return new Response(null, { status: 101, webSocket: client });
}
webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
// Echo the received message back to the client
ws.send(message);
}
webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {
// Handle WebSocket close events
}
}
Add a binding and migration for the Durable Object in your Wrangler configuration, alongside the existing COUNTER binding:
With the default options, hibernatable WebSockets remain open across eviction, so messages still round-trip afterwards. Passing { webSockets: "close" } closes them instead:
describe("WebSocket eviction behavior", () => { it("hibernates WebSockets across eviction by default", async () => { const id = env.WEBSOCKET_SERVER.idFromName("ws-test"); const stub = env.WEBSOCKET_SERVER.get(id);
const response = await stub.fetch("https://example.com", {
headers: { Upgrade: "websocket" },
});
const socket = response.webSocket;
if (!socket) throw new Error("Expected WebSocket response");
socket.accept();
// Hibernatable WebSockets are hibernated, not closed
await evictDurableObject(stub);
// Messages still round-trip after eviction wakes the Durable Object
const message = new Promise<string>((resolve) => {
socket.addEventListener("message", (event) => {
resolve(event.data as string);
});
});
socket.send("after-eviction");
expect(await message).toBe("after-eviction");
socket.close(1000, "done");
});
it("closes WebSockets when requested", async () => {
const id = env.WEBSOCKET_SERVER.idFromName("ws-close-test");
const stub = env.WEBSOCKET_SERVER.get(id);
const response = await stub.fetch("https://example.com", {
headers: { Upgrade: "websocket" },
});
const socket = response.webSocket;
if (!socket) throw new Error("Expected WebSocket response");
socket.accept();
const closed = new Promise<CloseEvent>((resolve) => {
socket.addEventListener("close", (event) => resolve(event));
});
// Close WebSockets instead of hibernating them
await evictDurableObject(stub, { webSockets: "close" });
expect(await closed).toBeDefined();
});
});
</TypeScriptExample>
To evict all currently-running Durable Objects at once (for example, to reset state between tests without deleting persisted data), use `evictAllDurableObjects()`:
```ts
import { evictAllDurableObjects } from "cloudflare:test";
import { afterEach } from "vitest";
afterEach(async () => {
await evictAllDurableObjects();
});
For more details on the eviction helpers, including the DurableObjectEvictionOptions interface, refer to the Test APIs reference.
Running tests
Run your tests with:
npx vitest
Or add a script to your package.json:
{
"scripts": {
"test": "vitest"
}
}
Related resources
- Workers Vitest integration - Full documentation for the Vitest integration
- Durable Objects testing recipe - Example from the Workers SDK
- RPC testing recipe - Testing JSRPC with Durable Objects
Use Workers KV from Durable Objects
import { GlossaryTooltip, TabItem, Tabs, WranglerConfig } from "~/components";
The following Worker script shows you how to configure a Durable Object to read from and/or write to a Workers KV namespace. This is useful when using a Durable Object to coordinate between multiple clients, and allows you to serialize writes to KV and/or broadcast a single read from KV to hundreds or thousands of clients connected to a single Durable Object using WebSockets.
Prerequisites:
- A KV namespace created via the Cloudflare dashboard or the wrangler CLI.
- A configured binding for the
kv_namespacein the Cloudflare dashboard or Wrangler file. - A Durable Object namespace binding.
Configure your Wrangler file as follows:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-worker",
"main": "src/index.ts",
"upload_source_maps": true,
"kv_namespaces": [
{
"binding": "YOUR_KV_NAMESPACE",
"id": "<id_of_your_namespace>"
}
],
"durable_objects": {
"bindings": [
{
"name": "YOUR_DO_CLASS",
"class_name": "YourDurableObject"
}
]
}
}
import { DurableObject } from "cloudflare:workers";
interface Env {
YOUR_KV_NAMESPACE: KVNamespace;
YOUR_DO_CLASS: DurableObjectNamespace;
}
export default {
async fetch(req: Request, env: Env): Promise<Response> {
// Assume each Durable Object is mapped to a roomId in a query parameter
// In a production application, this will likely be a roomId defined by your application
// that you validate (and/or authenticate) first.
let url = new URL(req.url);
let roomIdParam = url.searchParams.get("roomId");
if (roomIdParam) {
// Get a stub that allows you to call that Durable Object
let durableObjectStub = env.YOUR_DO_CLASS.getByName(roomIdParam);
// Pass the request to that Durable Object and await the response
// This invokes the constructor once on your Durable Object class (defined further down)
// on the first initialization, and the fetch method on each request.
//
// You could pass the original Request to the Durable Object's fetch method
// or a simpler URL with just the roomId.
let response = await durableObjectStub.fetch(`http://do/${roomId}`);
// This would return the value you read from KV *within* the Durable Object.
return response;
}
},
};
export class YourDurableObject extends DurableObject {
constructor(
public state: DurableObjectState,
env: Env,
) {
super(state, env);
}
async fetch(request: Request) {
// Error handling elided for brevity.
// Write to KV
await this.env.YOUR_KV_NAMESPACE.put("some-key");
// Fetch from KV
let val = await this.env.YOUR_KV_NAMESPACE.get("some-other-key");
return Response.json(val);
}
}
from workers import DurableObject, Response, WorkerEntrypoint
from urllib.parse import urlparse, parse_qs
class Default(WorkerEntrypoint):
async def fetch(self, req):
# Assume each Durable Object is mapped to a roomId in a query parameter
# In a production application, this will likely be a roomId defined by your application
# that you validate (and/or authenticate) first.
url = req.url
parsed_url = urlparse(url)
room_id_param = parse_qs(parsed_url.query).get('roomId', [None])[0]
if room_id_param:
# Get a stub that allows you to call that Durable Object
durable_object_stub = self.env.YOUR_DO_CLASS.getByName(room_id_param)
# Pass the request to that Durable Object and await the response
# This invokes the constructor once on your Durable Object class (defined further down)
# on the first initialization, and the fetch method on each request.
#
# You could pass the original Request to the Durable Object's fetch method
# or a simpler URL with just the roomId.
response = await durable_object_stub.fetch(f"http://do/{room_id_param}")
# This would return the value you read from KV *within* the Durable Object.
return response
class YourDurableObject(DurableObject):
def __init__(self, state, env):
super().__init__(state, env)
async def fetch(self, request):
# Error handling elided for brevity.
# Write to KV
await self.env.YOUR_KV_NAMESPACE.put("some-key", "some-value")
# Fetch from KV
val = await self.env.YOUR_KV_NAMESPACE.get("some-other-key")
return Response.json(val)
Build a WebSocket server with WebSocket Hibernation
import { TabItem, Tabs, WranglerConfig, TypeScriptExample } from "~/components";
This example is similar to the Build a WebSocket server example, but uses the WebSocket Hibernation API. The WebSocket Hibernation API should be preferred for WebSocket server applications built on Durable Objects, since it significantly decreases duration charge, and provides additional features that pair well with WebSocket applications. For more information, refer to Use Durable Objects with WebSockets.
:::note
WebSocket Hibernation is unavailable for outgoing WebSocket use cases. Hibernation is only supported when the Durable Object acts as a server. For use cases where outgoing WebSockets are required, refer to Write a WebSocket client.
:::
```ts import { DurableObject } from 'cloudflare:workers';// Worker export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { if (request.url.endsWith('/websocket')) { // Expect to receive a WebSocket Upgrade request. // If there is one, accept the request and return a WebSocket Response. const upgradeHeader = request.headers.get('Upgrade'); if (!upgradeHeader || upgradeHeader !== 'websocket') { return new Response('Worker expected Upgrade: websocket', { status: 426, }); }
if (request.method !== 'GET') {
return new Response('Worker expected GET method', {
status: 400,
});
}
// Since we are hard coding the Durable Object ID by providing the constant name 'foo',
// all requests to this Worker will be sent to the same Durable Object instance.
let stub = env.WEBSOCKET_HIBERNATION_SERVER.getByName("foo");
return stub.fetch(request);
}
return new Response(
`Supported endpoints:
/websocket: Expects a WebSocket upgrade request`, { status: 200, headers: { 'Content-Type': 'text/plain', }, } ); }, };
// Durable Object export class WebSocketHibernationServer extends DurableObject { // Keeps track of all WebSocket connections // When the DO hibernates, gets reconstructed in the constructor sessions: Map<WebSocket, { [key: string]: string }>;
constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.sessions = new Map();
// As part of constructing the Durable Object,
// we wake up any hibernating WebSockets and
// place them back in the `sessions` map.
// Get all WebSocket connections from the DO
this.ctx.getWebSockets().forEach((ws) => {
let attachment = ws.deserializeAttachment();
if (attachment) {
// If we previously attached state to our WebSocket,
// let's add it to `sessions` map to restore the state of the connection.
this.sessions.set(ws, { ...attachment });
}
});
// Sets an application level auto response that does not wake hibernated WebSockets.
this.ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair('ping', 'pong'));
}
async fetch(request: Request): Promise { // Creates two ends of a WebSocket connection. const webSocketPair = new WebSocketPair(); const [client, server] = Object.values(webSocketPair);
// Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating
// request within the Durable Object. It has the effect of "accepting" the connection,
// and allowing the WebSocket to send and receive messages.
// Unlike `ws.accept()`, `this.ctx.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket
// is "hibernatable", so the runtime does not need to pin this Durable Object to memory while
// the connection is open. During periods of inactivity, the Durable Object can be evicted
// from memory, but the WebSocket connection will remain open. If at some later point the
// WebSocket receives a message, the runtime will recreate the Durable Object
// (run the `constructor`) and deliver the message to the appropriate handler.
this.ctx.acceptWebSocket(server);
// Generate a random UUID for the session.
const id = crypto.randomUUID();
// Attach the session ID to the WebSocket connection and serialize it.
// This is necessary to restore the state of the connection when the Durable Object wakes up.
server.serializeAttachment({ id });
// Add the WebSocket connection to the map of active sessions.
this.sessions.set(server, { id });
return new Response(null, {
status: 101,
webSocket: client,
});
}
async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) { // Get the session associated with the WebSocket connection. const session = this.sessions.get(ws)!;
// Upon receiving a message from the client, the server replies with the same message, the session ID of the connection,
// and the total number of connections with the "[Durable Object]: " prefix
ws.send(`[Durable Object] message: ${message}, from: ${session.id}, to: the initiating client. Total connections: ${this.sessions.size}`);
// Send a message to all WebSocket connections, loop over all the connected WebSockets.
this.sessions.forEach((attachment, connectedWs) => {
connectedWs.send(`[Durable Object] message: ${message}, from: ${session.id}, to: all clients. Total connections: ${this.sessions.size}`);
});
// Send a message to all WebSocket connections except the connection (ws),
// loop over all the connected WebSockets and filter out the connection (ws).
this.sessions.forEach((attachment, connectedWs) => {
if (connectedWs !== ws) {
connectedWs.send(`[Durable Object] message: ${message}, from: ${session.id}, to: all clients except the initiating client. Total connections: ${this.sessions.size}`);
}
});
}
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) { // With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime // auto-replies to Close frames. Calling close() is safe but no longer required. ws.close(code, reason); this.sessions.delete(ws); } }
</TypeScriptExample>
<TabItem label="Python" icon="seti:python">
```py
from workers import DurableObject, Response, WorkerEntrypoint
from js import WebSocketPair, WebSocketRequestResponsePair
import uuid
class Session:
def __init__(self, *, ws):
self.ws = ws
# Worker
class Default(WorkerEntrypoint):
async def fetch(self, request):
if request.url.endswith('/websocket'):
# Expect to receive a WebSocket Upgrade request.
# If there is one, accept the request and return a WebSocket Response.
upgrade_header = request.headers.get('Upgrade')
if not upgrade_header or upgrade_header != 'websocket':
return Response('Worker expected Upgrade: websocket', status=426)
if request.method != 'GET':
return Response('Worker expected GET method', status=400)
# Since we are hard coding the Durable Object ID by providing the constant name 'foo',
# all requests to this Worker will be sent to the same Durable Object instance.
stub = self.env.WEBSOCKET_HIBERNATION_SERVER.getByName("foo")
return await stub.fetch(request)
return Response(
"""Supported endpoints:
/websocket: Expects a WebSocket upgrade request""",
status=200,
headers={'Content-Type': 'text/plain'}
)
# Durable Object
class WebSocketHibernationServer(DurableObject):
def __init__(self, ctx, env):
super().__init__(ctx, env)
# Keeps track of all WebSocket connections, keyed by session ID
# When the DO hibernates, gets reconstructed in the constructor
self.sessions = {}
# As part of constructing the Durable Object,
# we wake up any hibernating WebSockets and
# place them back in the `sessions` map.
# Get all WebSocket connections from the DO
for ws in self.ctx.getWebSockets():
attachment = ws.deserializeAttachment()
if attachment:
# If we previously attached state to our WebSocket,
# let's add it to `sessions` map to restore the state of the connection.
# Use the session ID as the key
self.sessions[attachment] = Session(ws=ws)
# Sets an application level auto response that does not wake hibernated WebSockets.
self.ctx.setWebSocketAutoResponse(WebSocketRequestResponsePair.new('ping', 'pong'))
async def fetch(self, request):
# Creates two ends of a WebSocket connection.
client, server = WebSocketPair.new().object_values()
# Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating
# request within the Durable Object. It has the effect of "accepting" the connection,
# and allowing the WebSocket to send and receive messages.
# Unlike `ws.accept()`, `this.ctx.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket
# is "hibernatable", so the runtime does not need to pin this Durable Object to memory while
# the connection is open. During periods of inactivity, the Durable Object can be evicted
# from memory, but the WebSocket connection will remain open. If at some later point the
# WebSocket receives a message, the runtime will recreate the Durable Object
# (run the `constructor`) and deliver the message to the appropriate handler.
self.ctx.acceptWebSocket(server)
# Generate a random UUID for the session.
id = str(uuid.uuid4())
# Attach the session ID to the WebSocket connection and serialize it.
# This is necessary to restore the state of the connection when the Durable Object wakes up.
server.serializeAttachment(id)
# Add the WebSocket connection to the map of active sessions, keyed by session ID.
self.sessions[id] = Session(ws=server)
return Response(None, status=101, web_socket=client)
async def webSocketMessage(self, ws, message):
# Get the session ID associated with the WebSocket connection.
session_id = ws.deserializeAttachment()
# Upon receiving a message from the client, the server replies with the same message, the session ID of the connection,
# and the total number of connections with the "[Durable Object]: " prefix
ws.send(f"[Durable Object] message: {message}, from: {session_id}, to: the initiating client. Total connections: {len(self.sessions)}")
# Send a message to all WebSocket connections, loop over all the connected WebSockets.
for session in self.sessions.values():
session.ws.send(f"[Durable Object] message: {message}, from: {session_id}, to: all clients. Total connections: {len(self.sessions)}")
# Send a message to all WebSocket connections except the connection (ws),
# loop over all the connected WebSockets and filter out the connection (ws).
for session in self.sessions.values():
if session.ws != ws:
session.ws.send(f"[Durable Object] message: {message}, from: {session_id}, to: all clients except the initiating client. Total connections: {len(self.sessions)}")
async def webSocketClose(self, ws, code, reason, wasClean):
# With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
# auto-replies to Close frames. Calling close() is safe but no longer required.
ws.close(code, reason)
# Get the session ID from the WebSocket attachment to remove it from sessions
session_id = ws.deserializeAttachment()
if session_id:
self.sessions.pop(session_id, None)
Finally, configure your Wrangler file to include a Durable Object binding and migration based on the namespace and class name chosen previously.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "websocket-hibernation-server",
"main": "src/index.ts",
"upload_source_maps": true,
"durable_objects": {
"bindings": [
{
"name": "WEBSOCKET_HIBERNATION_SERVER",
"class_name": "WebSocketHibernationServer"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"WebSocketHibernationServer"
]
}
]
}
Related resources
Build a WebSocket server
import { TabItem, Tabs, GlossaryTooltip, WranglerConfig, TypeScriptExample } from "~/components";
This example shows how to build a WebSocket server using Durable Objects and Workers. The example exposes an endpoint to create a new WebSocket connection. This WebSocket connection echos any message while including the total number of WebSocket connections currently established. For more information, refer to Use Durable Objects with WebSockets.
:::caution
WebSocket connections pin your Durable Object to memory, and so duration charges will be incurred so long as the WebSocket is connected (regardless of activity). To avoid duration charges during periods of inactivity, use the WebSocket Hibernation API, which only charges for duration when JavaScript is actively executing.
:::
```ts import { DurableObject } from 'cloudflare:workers';// Worker export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { if (request.url.endsWith('/websocket')) { // Expect to receive a WebSocket Upgrade request. // If there is one, accept the request and return a WebSocket Response. const upgradeHeader = request.headers.get('Upgrade'); if (!upgradeHeader || upgradeHeader !== 'websocket') { return new Response('Worker expected Upgrade: websocket', { status: 426, }); }
if (request.method !== 'GET') {
return new Response('Worker expected GET method', {
status: 400,
});
}
// Since we are hard coding the Durable Object ID by providing the constant name 'foo',
// all requests to this Worker will be sent to the same Durable Object instance.
let id = env.WEBSOCKET_SERVER.idFromName('foo');
let stub = env.WEBSOCKET_SERVER.get(id);
return stub.fetch(request);
}
return new Response(
`Supported endpoints:
/websocket: Expects a WebSocket upgrade request`, { status: 200, headers: { 'Content-Type': 'text/plain', }, } ); }, };
// Durable Object export class WebSocketServer extends DurableObject { // Keeps track of all WebSocket connections sessions: Map<WebSocket, { [key: string]: string }>;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sessions = new Map();
}
async fetch(request: Request): Promise<Response> {
// Creates two ends of a WebSocket connection.
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
// Calling `accept()` tells the runtime that this WebSocket is to begin terminating
// request within the Durable Object. It has the effect of "accepting" the connection,
// and allowing the WebSocket to send and receive messages.
server.accept();
// Generate a random UUID for the session.
const id = crypto.randomUUID();
// Add the WebSocket connection to the map of active sessions.
this.sessions.set(server, { id });
server.addEventListener('message', (event) => {
this.handleWebSocketMessage(server, event.data);
});
// When the client closes the connection, clean up the server side.
server.addEventListener('close', () => {
this.handleConnectionClose(server);
});
return new Response(null, {
status: 101,
webSocket: client,
});
}
async handleWebSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
const connection = this.sessions.get(ws)!;
// Reply back with the same message to the connection
ws.send(`[Durable Object] message: ${message}, from: ${connection.id}, to: the initiating client. Total connections: ${this.sessions.size}`);
// Broadcast the message to all the connections,
// except the one that sent the message.
this.sessions.forEach((_, session) => {
if (session !== ws) {
session.send(`[Durable Object] message: ${message}, from: ${connection.id}, to: all clients except the initiating client. Total connections: ${this.sessions.size}`);
}
});
// Broadcast the message to all the connections,
// including the one that sent the message.
this.sessions.forEach((_, session) => {
session.send(`[Durable Object] message: ${message}, from: ${connection.id}, to: all clients. Total connections: ${this.sessions.size}`);
});
}
async handleConnectionClose(ws: WebSocket) {
this.sessions.delete(ws);
// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
// auto-replies to Close frames. Calling close() is safe but no longer required.
ws.close(1000, 'Durable Object is closing WebSocket');
}
}
</TypeScriptExample>
<TabItem label="Python" icon="seti:python">
```py
from workers import DurableObject, Response, WorkerEntrypoint
from js import WebSocketPair
from pyodide.ffi import create_proxy
import uuid
class Session:
def __init__(self, *, ws):
self.ws = ws
# Worker
class Default(WorkerEntrypoint):
async def fetch(self, request):
if request.url.endswith('/websocket'):
# Expect to receive a WebSocket Upgrade request.
# If there is one, accept the request and return a WebSocket Response.
upgrade_header = request.headers.get('Upgrade')
if not upgrade_header or upgrade_header != 'websocket':
return Response('Worker expected Upgrade: websocket', status=426)
if request.method != 'GET':
return Response('Worker expected GET method', status=400)
# Since we are hard coding the Durable Object ID by providing the constant name 'foo',
# all requests to this Worker will be sent to the same Durable Object instance.
id = self.env.WEBSOCKET_SERVER.idFromName('foo')
stub = self.env.WEBSOCKET_SERVER.get(id)
return await stub.fetch(request)
return Response(
"""Supported endpoints:
/websocket: Expects a WebSocket upgrade request""",
status=200,
headers={'Content-Type': 'text/plain'}
)
# Durable Object
class WebSocketServer(DurableObject):
def __init__(self, ctx, env):
super().__init__(ctx, env)
# Keeps track of all WebSocket connections, keyed by session ID
self.sessions = {}
async def fetch(self, request):
# Creates two ends of a WebSocket connection.
client, server = WebSocketPair.new().object_values()
# Calling `accept()` tells the runtime that this WebSocket is to begin terminating
# request within the Durable Object. It has the effect of "accepting" the connection,
# and allowing the WebSocket to send and receive messages.
server.accept()
# Generate a random UUID for the session.
id = str(uuid.uuid4())
# Create proxies for event handlers (must be destroyed when socket closes)
async def on_message(event):
await self.handleWebSocketMessage(id, event.data)
message_proxy = create_proxy(on_message)
server.addEventListener('message', message_proxy)
# When the client closes the connection, clean up the server side.
async def on_close(event):
await self.handleConnectionClose(id)
# Clean up proxies
message_proxy.destroy()
close_proxy.destroy()
close_proxy = create_proxy(on_close)
server.addEventListener('close', close_proxy)
# Add the WebSocket connection to the map of active sessions, keyed by session ID.
self.sessions[id] = Session(ws=server)
return Response(None, status=101, web_socket=client)
async def handleWebSocketMessage(self, session_id, message):
session = self.sessions[session_id]
# Reply back with the same message to the connection
session.ws.send(f"[Durable Object] message: {message}, from: {session_id}, to: the initiating client. Total connections: {len(self.sessions)}")
# Broadcast the message to all the connections,
# except the one that sent the message.
for id, conn in self.sessions.items():
if id != session_id:
conn.ws.send(f"[Durable Object] message: {message}, from: {session_id}, to: all clients except the initiating client. Total connections: {len(self.sessions)}")
# Broadcast the message to all the connections,
# including the one that sent the message.
for id, conn in self.sessions.items():
conn.ws.send(f"[Durable Object] message: {message}, from: {session_id}, to: all clients. Total connections: {len(self.sessions)}")
async def handleConnectionClose(self, session_id):
session = self.sessions.pop(session_id, None)
if session:
# With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
# auto-replies to Close frames. Calling close() is safe but no longer required.
session.ws.close(1000, 'Durable Object is closing WebSocket')
Finally, configure your Wrangler file to include a Durable Object binding and migration based on the namespace and class name chosen previously.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "websocket-server",
"main": "src/index.ts",
"upload_source_maps": true,
"durable_objects": {
"bindings": [
{
"name": "WEBSOCKET_SERVER",
"class_name": "WebSocketServer"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"WebSocketServer"
]
}
]
}
Related resources
Data Studio
Each Durable Object can access private storage using Storage API available on ctx.storage. To view and write to an object's stored data, you can use Durable Objects Data Studio as a UI editor available on the Cloudflare dashboard.
:::note[Data Studio only supported for SQLite-backed objects]
You can only use Data Studio to access data for SQLite-backed Durable Objects.
At the moment, you can only read/write data persisted using the SQL API. Key-value data persisted using the KV API will be made read-only in the future. :::
View Data Studio
You need at least Editor access to the Worker that implements the Durable Object to access Data Studio. Refer to Durable Objects roles and permissions for more information.
- Queries executed by Data Studio send requests to your remote, deployed objects and incur usage billing for requests, duration, rows read, and rows written. You should use Data Studio as you would handle your production, running objects.
- In the Query tab when running all statements, each SQL statement is sent as a separate Durable Object request.
Audit logging
All queries issued by the Data Studio are logged with audit logging v1 for your security and compliance needs.
- Each query emits two audit logs, a
query executedaction and aquery completedaction indicating query success or failure.query_idin the log event can be used to correlate the two events per query.
Observability
import { DirectoryListing } from "~/components";
Metrics and analytics
import { GlossaryTooltip, DashButton, Steps, WranglerConfig, Render } from "~/components";
Durable Objects expose analytics for Durable Object namespace-level and request-level metrics.
The metrics displayed in the Cloudflare dashboard charts are queried from Cloudflare's GraphQL Analytics API. You can access the metrics programmatically via GraphQL or HTTP client.
:::note[Durable Object namespace]
A Durable Object namespace is a set of Durable Objects that can be addressed by name, backed by the same class. There is only one Durable Object namespace per class. A Durable Object namespace can contain any number of Durable Objects. :::
View metrics and analytics
Per-namespace analytics for Durable Objects are available in the Cloudflare dashboard. To view current and historical metrics for a namespace:
1. In the Cloudflare dashboard, go to the **Durable Objects** page. 2. View account-level Durable Objects usage. 3. Select an existing Durable Object namespace. 4. Select the **Metrics** tab.You can optionally select a time window to query. This defaults to the last 24 hours.
You can also filter the charts to a single Durable Object by entering its ID or name and selecting a match. Clear the filter to return to namespace-level metrics.
Memory usage
The Memory usage chart on the Metrics tab shows V8 isolate memory usage, sampled periodically while your Durable Objects are active, broken down into P50, P90, P99, and P999 percentiles. Each isolate is subject to a 128 MB memory limit.
This memory holds the in-memory state your objects accumulate — such as class properties, caches, and active WebSocket connections — which persists across requests until an object is hibernated or evicted. This state is not preserved across eviction, hibernation, or a crash, so persist anything important to storage.
:::note[Memory is measured per isolate, not per Durable Object]
A single isolate can host multiple Durable Objects of the same class, along with the surrounding Worker code, and they all share that isolate's memory. The chart always reports the memory of the whole isolate, not an individual Durable Object. :::
What the chart shows depends on whether you filter:
- Without a filter (namespace view): the percentiles are computed across the periodic memory samples of every Durable Object in the namespace, showing the distribution of isolate memory across the namespace.
- Filtered by ID or name: the percentiles are computed only from the periodic samples reported for that one Durable Object. Each sample is still the memory of the entire isolate hosting it — which may include other Durable Objects sharing that isolate — so this is not a measurement of that single object's memory in isolation.
Memory usage is powered by the durableObjectsPeriodicGroups GraphQL dataset, which exposes the memoryUsageBytes metric. Percentile values are available as quantiles.memoryUsageBytesP50 through quantiles.memoryUsageBytesP999, in bytes.
If you see memory usage trending upward over time, this may indicate a memory leak. Use memory profiling with DevTools locally to take heap snapshots and identify specific objects causing high memory consumption.
To identify allocating code in a deployed Durable Object, capture a Heap profile in production.
View logs
You can view Durable Object logs from the Cloudflare dashboard. Logs are aggregated by the script name and the Durable Object class name.
To start using Durable Object logging:
1. Enable Durable Object logging in the Wrangler configuration file of the Worker that defines your Durable Object class: ```jsonc { "observability": { "enabled": true } } ``` 2. Deploy the latest version of the Worker with the updated binding. 3. Go to the **Durable Objects** page. 4. Select an existing Durable Object namespace. 5. Select the **Observability** tab.:::note For information on log limits (such as maximum log retention period), refer to the Workers Logs documentation. :::
Query via the GraphQL API
Durable Object metrics are powered by GraphQL.
The datasets that include Durable Object metrics include:
durableObjectsInvocationsAdaptiveGroupsdurableObjectsPeriodicGroupsdurableObjectsStorageGroupsdurableObjectsSubrequestsAdaptiveGroups
Use GraphQL Introspection to get information on the fields exposed by each datasets.
WebSocket metrics
Durable Objects using WebSockets will see request metrics across several GraphQL datasets because WebSockets have different types of requests.
- Metrics for a WebSocket connection itself is represented in
durableObjectsInvocationsAdaptiveGroupsonce the connection closes. Since WebSocket connections are long-lived, connections often do not terminate until the Durable Object terminates. - Metrics for incoming and outgoing WebSocket messages on a WebSocket connection are available in
durableObjectsPeriodicGroups. If a WebSocket connection uses WebSocket Hibernation, incoming WebSocket messages are instead represented indurableObjectsInvocationsAdaptiveGroups.
Example GraphQL query for Durable Objects
viewer {
/*
Replace with your account tag, the 32 hex character id visible at the beginning of any url
when logged in to dash.cloudflare.com or under "Account ID" on the sidebar of the Workers & Pages Overview
*/
accounts(filter: {accountTag: "your account tag here"}) {
// Replace dates with a recent date
durableObjectsInvocationsAdaptiveGroups(filter: {date_gt: "2023-05-23"}, limit: 1000) {
sum {
// Any other fields found through introspection can be added here
requests
responseBodySize
}
}
durableObjectsPeriodicGroups(filter: {date_gt: "2023-05-23"}, limit: 1000) {
sum {
cpuTime
}
}
durableObjectsStorageGroups(filter: {date_gt: "2023-05-23"}, limit: 1000) {
max {
storedBytes
}
}
}
}
Refer to the Querying Workers Metrics with GraphQL tutorial for authentication and to learn more about querying Workers datasets.
Additional resources
- For instructions on setting up a Grafana dashboard to query Cloudflare's GraphQL Analytics API, refer to Grafana Dashboard starter for Durable Object metrics.
FAQs
Troubleshooting
wrangler dev and wrangler tail are both available to help you debug your Durable Objects.
wrangler tail displays a live feed of console and exception logs for each request served by your Worker code, including both normal Worker requests and Durable Object requests. After running npx wrangler deploy, you can use wrangler tail in the root directory of your Worker project and visit your Worker URL to see console and error logs in your terminal.
Common errors
No event handlers were registered. This script does nothing.
In your Wrangler file, make sure the dir and main entries point to the correct file containing your Worker code, and that the file extension is .mjs instead of .js if using ES modules syntax.
Cannot apply --delete-class migration to class.
When deleting a migration using npx wrangler deploy --delete-class <ClassName>, you may encounter this error: "Cannot apply --delete-class migration to class <ClassName> without also removing the binding that references it". You should remove the corresponding binding under [durable_objects] in the Wrangler configuration file before attempting to apply --delete-class again.
Durable Object is overloaded.
A single instance of a Durable Object cannot do more work than is possible on a single thread. These errors mean the Durable Object has too much work to keep up with incoming requests:
Error: Durable Object is overloaded. Too many requests queued.The total count of queued requests is too high.Error: Durable Object is overloaded. Too much data queued.The total size of data in queued requests is too high.Error: Durable Object is overloaded. Requests queued for too long.The oldest request has been in the queue too long.Error: Durable Object is overloaded. Too many requests for the same object within a 10 second window.The number of requests for a Durable Object is too high within a short span of time (10 seconds). This error indicates a more extreme level of overload.
To solve this error, you can either do less work per request, or send fewer requests. For example, you can split the requests among more instances of the Durable Object.
These errors and others that are due to overload will have an .overloaded property set on their exceptions, which can be used to avoid retrying overloaded operations.
Your account is generating too much load on Durable Objects. Please back off and try again later.
There is a limit on how quickly you can create new stubs for new or existing Durable Objects. Those lookups are usually cached, meaning attempts for the same set of recently accessed Durable Objects should be successful, so catching this error and retrying after a short wait is safe. If possible, also consider spreading those lookups across multiple requests.
Durable Object reset because its code was updated.
This error occurs when a code update is applied before a Durable Object reaches hibernation. This includes an immediate code update strategy and deferred deployments whose max_delay is reached before the object hibernates.
Reset in error messages refers to in-memory state. Any durable state already persisted with ctx.storage is not affected. Make operations retryable and persist important state so a forced update can safely restart the object.
Refer to Durable Objects code updates and Global uniqueness.
Durable Object storage operation exceeded timeout which caused object to be reset.
To prevent indefinite blocking, there is a limit on how much time storage operations can take. In Durable Objects containing a sufficiently large number of key-value pairs, deleteAll() may hit that time limit and fail. When this happens, note that each deleteAll() call does make progress and that it is safe to retry until it succeeds.
Your account is doing too many concurrent storage operations. Please back off and try again later.
Besides the suggested approach of backing off, also consider changing your code to use state.storage.get(keys Array<string>) rather than multiple individual state.storage.get(key) calls where possible.
Platform
import { DirectoryListing } from "~/components";
Known issues
import { GlossaryTooltip } from "~/components";
Durable Objects is generally available. However, there are some known issues.
Global uniqueness
Global uniqueness guarantees there is only a single instance of a Durable Object class with a given ID running at once, across the world.
Uniqueness is enforced upon starting a new event (such as receiving an HTTP request), and upon accessing storage.
After an event is received, if the event takes some time to execute and does not ever access its durable storage, then it is possible that the Durable Object may no longer be current, and some other instance of the same Durable Object ID will have been created elsewhere. If the event accesses storage at this point, it will receive an exception. If the event completes without ever accessing storage, it may not ever realize that the Durable Object was no longer current.
A Durable Object may be replaced in the event of a network partition or a software update (including either an update of the Durable Object's class code, or of the Workers system itself). Enabling wrangler tail or Cloudflare dashboard logs requires a software update.
Code updates
Code changes for Workers and Durable Objects are released globally in an eventually consistent manner. A request can reach the latest version of your Worker, which then calls a unique Durable Object running a previous version of your code.
With a deferred code update strategy, an active Durable Object can continue running a previous version until it hibernates or its max_delay is reached. Gradual deployments can extend this window while your deployment uses more than one version. If you deploy multiple versions before an object hibernates, it might skip intermediate versions and use the latest version when it wakes.
Ensure that API changes between your Workers and Durable Objects are forward and backward compatible across code updates.
Development tools
wrangler tail logs from requests that are upgraded to WebSockets are delayed until the WebSocket is closed. wrangler tail should not be connected to a Worker that you expect will receive heavy volumes of traffic.
The Workers editor in the Cloudflare dashboard allows you to interactively edit and preview your Worker and Durable Objects. In the editor, Durable Objects can only be talked to by a preview request if the Worker being previewed both exports the Durable Object class and binds to it. Durable Objects exported by other Workers cannot be talked to in the editor preview.
wrangler dev has read access to Durable Object storage, but writes will be kept in memory and will not affect persistent data. However, if you specify the script_name explicitly in the Durable Object binding, then writes will affect persistent data. Wrangler will emit a warning in that case.
Alarms in local development
Currently, when developing locally (using npx wrangler dev), Durable Object alarm methods may fail after a hot reload (if you edit the code while the code is running locally).
To avoid this issue, when using Durable Object alarms, close and restart your wrangler dev command after editing your code.
Limits
import { Render, GlossaryTooltip, WranglerConfig } from "~/components";
Durable Objects are a special kind of Worker, so Workers limits also apply. Limits that depend on your Workers plan are identified on that page. This page also covers Durable Object storage, CPU, and outbound HTTP request body size limits.
SQLite-backed Durable Objects general limits
| Feature | Limit |
|---|---|
| Number of Objects | Unlimited (within an account or of a given class) |
| Maximum Durable Object classes (per account) | 500 (Workers Paid) / 100 (Free) [^1] |
| Storage per account | Unlimited (Workers Paid) / 5GB (Free) [^2] |
| Storage per class | Unlimited [^3] |
| Storage per Durable Object | 10 GB [^3] |
| Key size | Key and value combined cannot exceed 2 MB |
| Value size | Key and value combined cannot exceed 2 MB |
| WebSocket message size | 32 MiB (only for received messages) |
| CPU per request | 30 seconds (default) / configurable to 5 minutes of active CPU time [^4] |
| Simultaneous outgoing connections/request | 6 (same as Workers) |
[^1]: Identical to the Workers script limit.
[^2]: Durable Objects both bills and measures storage based on a gigabyte
(1 GB = 1,000,000,000 bytes) and not a gibibyte (GiB).
[^3]: Accounts on the Workers Free plan are limited to 5 GB total Durable Objects storage.
[^4]: Each incoming HTTP request or WebSocket message resets the remaining available CPU time to 30 seconds. This allows the Durable Object to consume up to 30 seconds of compute after each incoming network request, with each new network request resetting the timer. If you consume more than 30 seconds of compute between incoming network requests, there is a heightened chance that the individual Durable Object is evicted and reset. CPU time per request invocation can be increased.
SQL storage limits
For Durable Object classes with SQLite storage these SQL limits apply:
| SQL | Limit |
|---|---|
| Maximum number of columns per table | 100 |
| Maximum number of rows per table | Unlimited (excluding per-object storage limits) |
Maximum string, BLOB or table row size |
2 MB |
| Maximum SQL statement length | 100 KB |
| Maximum bound parameters per query | 100 |
| Maximum arguments per SQL function | 32 |
Maximum characters (bytes) in a LIKE or GLOB pattern |
50 bytes |
Key-value backed Durable Objects general limits
| Feature | Limit for class with key-value storage backend |
|---|---|
| Number of Objects | Unlimited (within an account or of a given class) |
| Maximum Durable Object classes (per account) | 500 (Workers Paid) / 100 (Free) [^5] |
| Storage per account | 50 GB (can be raised by contacting Cloudflare) [^6] |
| Storage per class | Unlimited |
| Storage per Durable Object | Unlimited |
| Key size | 2 KiB (2048 bytes) |
| Value size | 128 KiB (131072 bytes) |
| WebSocket message size | 32 MiB (only for received messages) |
| CPU per request | 30s (including WebSocket messages) [^7] |
| Simultaneous outgoing connections/request | 6 (same as Workers) |
[^5]: Identical to the Workers script limit.
[^6]: Durable Objects both bills and measures storage based on a gigabyte
(1 GB = 1,000,000,000 bytes) and not a gibibyte (GiB).
[^7]: Each incoming HTTP request or WebSocket message resets the remaining available CPU time to 30 seconds. This allows the Durable Object to consume up to 30 seconds of compute after each incoming network request, with each new network request resetting the timer. If you consume more than 30 seconds of compute between incoming network requests, there is a heightened chance that the individual Durable Object is evicted and reset. CPU time per request invocation can be increased.
Outbound HTTP request body size
Cloudflare limits each HTTP request body a Durable Object sends to an external service with fetch() to 100 MiB by default. Requests exceeding this limit can fail with 413 Payload Too Large.
Frequently Asked Questions
Pricing
import { Render } from "~/components";
Durable Objects can incur two types of billing: compute and storage.
On Workers Free plan:
- If you exceed any one of the free tier limits, further operations of that type will fail with an error.
- Daily free limits reset at 00:00 UTC.
<Render file="durable-objects-pricing" product="durable-objects" params={{ computeDepth: 2, storageDepth: 2, backendDepth: 3 }} />
Compute billing examples
These examples exclude the costs for the Workers calling the Durable Objects. When modelling the costs of a Durable Object, note that:
- Inactive objects receiving no requests do not incur any duration charges.
- The WebSocket Hibernation API can dramatically reduce duration-related charges for Durable Objects communicating with clients over the WebSocket protocol, especially if messages are only transmitted occasionally at sparse intervals.
- Pending I/O operations keep an object in memory for up to 15 minutes each. The object incurs duration charges during that time. For more information, refer to Lifecycle of a Durable Object.
Example 1
This example represents a simple Durable Object used as a co-ordination service invoked via HTTP.
- A single Durable Object was called by a Worker 1.5 million times
- It is active for 1,000,000 seconds in the month
In this scenario, the estimated monthly cost would be calculated as:
Requests:
- 1.5 million requests - included 1 million requests = 500,000 billable requests.
- (rounded) 1,000,000 requests x $0.15 / 1,000,000 = $0.15.
Compute Duration:
- 1,000,000 seconds * 128 MB / 1 GB = 128,000 GB-s.
- 128,000 GB-s is within the 400,000 GB-s included allocation = $0.00.
Estimated total: $0.15 (requests) + $0.00 (compute duration) + minimum $5/mo usage = $5.15 per month
Example 2
This example represents a moderately trafficked Durable Objects based application using WebSockets to broadcast game, chat or real-time user state across connected clients:
- 100 Durable Objects have 50 WebSocket connections established to each of them.
- Clients send approximately one message a minute for eight active hours a day, every day of the month.
In this scenario, the estimated monthly cost would be calculated as:
Requests:
- 50 WebSocket connections * 100 Durable Objects to establish the WebSockets = 5,000 connections created each day * 30 days = 150,000 WebSocket connection requests.
- 50 messages per minute * 100 Durable Objects * 60 minutes * 8 hours * 30 days = 72,000,000 WebSocket message requests.
- 150,000 + (72 million requests / 20 for WebSocket message billing ratio) = 3.75 million billing request.
- 3.75 million requests - included 1 million requests = 2,750,000 billable requests.
- (rounded) 3,000,000 requests x $0.15 / 1,000,000 = $0.45.
Compute Duration:
- 100 Durable Objects * 60 seconds * 60 minutes * 8 hours * 30 days = 86,400,000 seconds.
- 86,400,000 seconds * 128 MB / 1 GB = 11,059,200 GB-s.
- 11,059,200 GB-s - included 400,000 GB-s = 10,659,200 GB-s
- (rounded) 11,000,000 GB-s x $12.50 / 1,000,000 = $137.50.
Estimated total: $0.45 (requests) + $137.50 (compute duration) + minimum $5/mo usage = $142.95 per month.
Example 3
This example represents a horizontally scaled Durable Objects based application using WebSockets to communicate user-specific state to a single client connected to each Durable Object.
- 100 Durable Objects each have a single WebSocket connection established to each of them.
- Clients sent one message every second of the month so that the Durable Objects were active for the entire month.
In this scenario, the estimated monthly cost would be calculated as:
Requests:
- 100 WebSocket connection requests.
- 1 message per second * 100 connections * 60 seconds * 60 minutes * 24 hours * 30 days = 259,200,000 WebSocket message requests.
- 100 + (259.2 million requests / 20 for WebSocket billing ratio) = 12,960,100 requests.
- 12,960,100 requests - included 1 million requests = 11,960,100 billable requests.
- (rounded) 12,000,000 requests x $0.15 / 1,000,000 = $1.80.
Compute Duration:
- 100 Durable Objects * 60 seconds * 60 minutes * 24 hours * 30 days = 259,200,000 seconds
- 259,200,000 seconds * 128 MB / 1 GB = 33,177,600 GB-s
- 33,177,600 GB-s - included 400,000 GB-s = 32,777,600 GB-s
- (rounded) 33,000,000 GB-s x $12.50 / 1,000,000 = $412.50
Estimated total: $1.80 (requests) + $412.50 (compute duration) + minimum $5/mo usage = $419.30 per month
Example 4
This example represents a moderately trafficked Durable Objects based application using WebSocket Hibernation to broadcast game, chat or real-time user state across connected clients:
- 100 Durable Objects each have 100 Hibernatable WebSocket connections established to each of them.
- Clients send one message per minute, and it takes 10ms to process a single message in the
webSocketMessage()handler. Since each Durable Object handles 100 WebSockets, cumulatively each Durable Object will be actively executing JS for 1 second each minute (100 WebSockets * 10ms).
In this scenario, the estimated monthly cost would be calculated as:
Requests:
- 100 WebSocket connections * 100 Durable Objects to establish the WebSockets = 10,000 initial WebSocket connection requests.
- 100 messages per minute1 * 100 Durable Objects * 60 minutes * 24 hours * 30 days = 432,000,000 requests.
- 10,000 + (432 million requests / 20 for WebSocket billing ratio) = 21,610,000 requests.
- 21,610,000 requests - included 1 million requests = 20,610,000 billable requests.
- (rounded) 21,000,000 requests x $0.15 / 1,000,000 = $3.15.
Compute Duration:
- 100 Durable Objects * 1 second2 * 60 minutes * 24 hours * 30 days = 4,320,000 seconds
- 4,320,000 seconds * 128 MB / 1 GB = 552,960 GB-s
- 552,960 GB-s - included 400,000 GB-s = 152,960 GB-s
- (rounded) 1,000,000 GB-s x $12.50 / 1,000,000 = $12.50
Estimated total: $3.15 (requests) + $12.50 (compute duration) + minimum $5/mo usage = $20.65 per month
1 100 messages per minute comes from the fact that 100 clients connect to each DO, and each sends 1 message per minute.
2 The example uses 1 second because each Durable Object is active for 1 second per minute. This can also be thought of as 432 million requests that each take 10 ms to execute (4,320,000 seconds).
Frequently Asked Questions
Data location
import { GlossaryTooltip } from "~/components";
Restrict Durable Objects to a jurisdiction
Jurisdictions are used to create Durable Objects that only run and store data within a region to comply with local regulations such as the GDPR or FedRAMP.
Workers may still access Durable Objects constrained to a jurisdiction from anywhere in the world. The jurisdiction constraint only controls where the Durable Object itself runs and persists data. Consider using Regional Services to control the regions from which Cloudflare responds to requests.
:::note[Logging]
A DurableObjectId will be logged outside of the specified jurisdiction for billing and debugging purposes.
:::
Durable Objects can be restricted to a specific jurisdiction by creating a DurableObjectNamespace restricted to a jurisdiction. All Durable Object ID methods are valid on IDs within a namespace restricted to a jurisdiction.
const euSubnamespace = env.MY_DURABLE_OBJECT.jurisdiction("eu");
const euId = euSubnamespace.newUniqueId();
-
It is possible to have the same name represent different IDs in different jurisdictions.
const euId1 = env.MY_DURABLE_OBJECT.idFromName("my-name"); const euId2 = env.MY_DURABLE_OBJECT.jurisdiction("eu").idFromName("my-name"); console.assert(!euId1.equal(euId2), "This should always be true"); -
You will run into an error if the jurisdiction on your
DurableObjectNamespaceand the jurisdiction onDurableObjectIdare different. -
You will not run into an error if the
DurableObjectNamespaceis not associated with a jurisdiction. -
All Durable Object ID methods are valid on IDs within a namespace restricted to a jurisdiction.
const euSubnamespace = env.MY_DURABLE_OBJECT.jurisdiction("eu"); const euId = euSubnamespace.idFromName(name); const stub = env.MY_DURABLE_OBJECT.get(euId);
:::caution[Use DurableObjectNamespace.jurisdiction]
When specifying a jurisdiction, Cloudflare recommends you first create a namespace restricted to a jurisdiction, using const euSubnamespace = env.MY_DURABLE_OBJECT.jurisdiction("eu").
Note that it is also possible to specify a jurisdiction by creating an individual DurableObjectId restricted to a jurisdiction, using const euId = env.MY_DURABLE_OBJECT.newUniqueId({ jurisdiction: "eu" }).
However, Cloudflare does not recommend this approach. :::
Supported locations
| Parameter | Location |
|---|---|
| eu | The European Union |
| us | The United States |
| fedramp | FedRAMP-Moderate data centers |
Read the jurisdiction from inside a Durable Object
The jurisdiction of a Durable Object is available inside the object via ctx.id.jurisdiction. The value is preserved across toString() and idFromString() round-trips and is also available inside alarm handlers for alarms scheduled on 2026-03-15 or later, which makes it suitable for region-aware logic inside the Durable Object.
Provide a location hint
Durable Objects, as with any stateful API, will often add response latency as requests must be forwarded to the data center where the Durable Object, or state, is located.
Durable Objects do not currently change locations after they are created1. By default, a Durable Object is instantiated in a data center close to where the initial get() request is made. This may not be in the same data center that the get() request is made from, but in most cases, it will be in close proximity.
:::caution[Initial requests to Durable Objects]
It can negatively impact latency to pre-create Durable Objects prior to the first client request or when the first client request is not representative of where the majority of requests will come from. It is better for latency to create Durable Objects in response to actual production traffic or provide explicit location hints.
:::
Location hints are the mechanism provided to specify the location that a Durable Object should be located regardless of where the initial get() request comes from.
To manually create Durable Objects in another location, provide an optional locationHint parameter to get(). Only the first call to get() for a particular Object will respect the hint.
let durableObjectStub = OBJECT_NAMESPACE.get(id, { locationHint: "enam" });
:::caution
Hints are a best effort and not a guarantee. Unlike with jurisdictions, Durable Objects will not necessarily be instantiated in the hinted location, but instead instantiated in a data center selected to minimize latency from the hinted location. :::
Supported locations
| Parameter | Location |
|---|---|
| wnam | Western North America |
| enam | Eastern North America |
| sam | South America 2 |
| weur | Western Europe |
| eeur | Eastern Europe |
| apac | Asia-Pacific |
| apac-ne | Northeast Asia-Pacific 3 |
| apac-se | Southeast Asia-Pacific 3 |
| oc | Oceania |
| afr | Africa 2 |
| me | Middle East 2 |
1 Dynamic relocation of existing Durable Objects is planned for the future.
2 Durable Objects currently do not spawn in this location. Instead, the Durable Object will spawn in a nearby location which does support Durable Objects. For example, Durable Objects hinted to South America spawn in Eastern North America instead.
3 apac-ne (Northeast Asia-Pacific) and apac-se (Southeast
Asia-Pacific) are narrower sub-regions within apac (Asia-Pacific). Prefer
apac for general Asia-Pacific placement. Choose apac-ne or apac-se only
when your users are concentrated in the northeast or southeast of the region and
you want to minimize latency for them.
Additional resources
- You can find our more about where Durable Objects are located using the website: Where Durable Objects Live.
Data security
This page details the data security properties of Durable Objects, including:
- Encryption-at-rest (EAR).
- Encryption-in-transit (EIT).
- Cloudflare's compliance certifications.
Encryption at Rest
All Durable Object data, including metadata, is encrypted at rest. Encryption and decryption are automatic, do not require user configuration to enable, and do not impact the effective performance of Durable Objects.
Encryption keys are managed by Cloudflare and securely stored in the same key management systems we use for managing encrypted data across Cloudflare internally.
Encryption at rest is implemented using the Linux Unified Key Setup (LUKS) disk encryption specification and AES-256, a widely tested, highly performant and industry-standard encryption algorithm.
Encryption in Transit
Data transfer between a Cloudflare Worker, and/or between nodes within the Cloudflare network and Durable Objects is secured using the same Transport Layer Security (TLS/SSL).
API access via the HTTP API or using the wrangler command-line interface is also over TLS/SSL (HTTPS).
Compliance
To learn more about Cloudflare's adherence to industry-standard security compliance certifications, visit the Cloudflare Trust Hub.
Durable Object class migrations (legacy)
import { Render, GlossaryTooltip, WranglerConfig, Steps, Details, } from "~/components";
:::note[Prefer declarative exports for new Workers]
For new Workers, use the declarative exports field instead of the migrations array described on this page. The migrations array remains fully supported for existing Workers and continues to work as documented here.
You cannot use migrations and Durable Object entries in exports in the same Worker configuration. Workflow entries in exports can be used alongside migrations. To move an existing Worker from migrations to exports, refer to Migrate from migrations to exports.
:::
A migration is a mapping process from a class name to a runtime state. This process communicates the changes to the Workers runtime and provides the runtime with instructions on how to deal with those changes.
To apply a migration, you need to:
- Edit your Wrangler configuration file (refer to Migration Wrangler configuration).
- Re-deploy your Worker using
npx wrangler deploy.
You must initiate a migration process when you:
- Create a new Durable Object class.
- Rename a Durable Object class.
- Delete a Durable Object class.
- Transfer an existing Durable Objects class.
:::note
Updating the code for an existing Durable Object class does not require a migration. To update the code for an existing Durable Object class, run npx wrangler deploy. This is true even for changes to how the code interacts with persistent storage. Because of global uniqueness, you do not have to be concerned about old and new code interacting with the same storage simultaneously. However, it is your responsibility to ensure that the new code is backwards compatible with existing stored data.
:::
Create migration
The most common migration performed is a new class migration, which informs the runtime that a new Durable Object class is being uploaded. This is also the migration you need when creating your first Durable Object class.
To apply a Create migration:
1. Add the following lines to your Wrangler configuration file:<WranglerConfig>
```jsonc
{
"migrations": [
{
"tag": "<v1>", // Migration identifier. This should be unique for each migration entry
"new_sqlite_classes": [ // Array of new classes
"<NewDurableObjectClass>"
]
}
]
}
```
</WranglerConfig>
The Create migration contains:
- A `tag` to identify the migration.
- The array `new_sqlite_classes`, which contains the new Durable Object class.
- Ensure you reference the correct name of the Durable Object class in your Worker code.
- Deploy the Worker.
To create a new Durable Object binding DURABLE_OBJECT_A, your Wrangler configuration file should look like the following:
Create Durable Object class with key-value storage
Use new_classes on the migration in your Worker's Wrangler file to create a Durable Object class with the key-value storage backend:
{
"migrations": [
{
"tag": "v1", // Should be unique for each entry
"new_classes": [
// Array of new classes
"MyDurableObject",
],
},
],
}
Delete migration
Running a Delete migration will delete all Durable Objects associated with the deleted class, including all of their stored data.
- Do not run a Delete migration on a class without first ensuring that you are not relying on the Durable Objects within that Worker anymore, that is, first remove the binding from the Worker.
- Copy any important data to some other location before deleting.
- You do not have to run a Delete migration on a class that was renamed or transferred.
To apply a Delete migration:
1. Remove the binding for the class you wish to delete from the Wrangler configuration file. 2. Remove references for the class you wish to delete from your Worker code. 3. Add the following lines to your Wrangler configuration file.<WranglerConfig>
```jsonc
{
"migrations": [
{
"tag": "<v2>", // Migration identifier. This should be unique for each migration entry
"deleted_classes": [ // Array of deleted class names
"<ClassToDelete>"
]
}
]
}
```
</WranglerConfig>
The Delete migration contains:
- A `tag` to identify the migration.
- The array `deleted_classes`, which contains the deleted Durable Object classes.
- Deploy the Worker.
To delete a Durable Object binding `DEPRECATED_OBJECT`, your Wrangler configuration file should look like the following: ```jsonc { // Remove the binding for the DeprecatedObjectClass DO // {"durable_objects": {"bindings": [ // { // "name": "DEPRECATED_OBJECT", // "class_name": "DeprecatedObjectClass" // } // ]}} "migrations": [ { "tag": "v3", // Should be unique for each entry "deleted_classes": [ // Array of deleted classes "DeprecatedObjectClass" ] } ] } ```
Rename migration
Rename migrations are used to transfer stored Durable Objects between two Durable Object classes in the same Worker code file.
To apply a Rename migration:
1. Update the previous class name to the new class name by editing your Wrangler configuration file in the following way:<WranglerConfig>
```jsonc
{
"durable_objects": {
"bindings": [
{
"name": "<MY_DURABLE_OBJECT>",
"class_name": "<UpdatedDurableObject>" // Update the class name to the new class name
}
]
},
"migrations": [
{
"tag": "<v3>", // Migration identifier. This should be unique for each migration entry
"renamed_classes": [ // Array of rename directives
{
"from": "<OldDurableObject>",
"to": "<UpdatedDurableObject>"
}
]
}
]
}
```
</WranglerConfig>
The Rename migration contains:
- A `tag` to identify the migration.
- The `renamed_classes` array, which contains objects with `from` and `to` properties.
- `from` property is the old Durable Object class name.
- `to` property is the renamed Durable Object class name.
- Reference the new Durable Object class name in your Worker code.
- Deploy the Worker.
To rename a Durable Object class, from OldName to UpdatedName, your Wrangler configuration file should look like the following:
Transfer migration
Transfer migrations are used to transfer stored Durable Objects between two Durable Object classes in different Worker code files.
If you want to transfer stored Durable Objects between two Durable Object classes in the same Worker code file, use Rename migrations instead.
:::note Do not run a Create migration for the destination class before running a Transfer migration. The Transfer migration will create the destination class for you. :::
To apply a Transfer migration:
1. Edit your Wrangler configuration file in the following way:<WranglerConfig>
```jsonc
{
"durable_objects": {
"bindings": [
{
"name": "<MY_DURABLE_OBJECT>",
"class_name": "<DestinationDurableObjectClass>"
}
]
},
"migrations": [
{
"tag": "<v4>", // Migration identifier. This should be unique for each migration entry
"transferred_classes": [
{
"from": "<SourceDurableObjectClass>",
"from_script": "<SourceWorkerScript>",
"to": "<DestinationDurableObjectClass>"
}
]
}
]
}
```
</WranglerConfig>
The Transfer migration contains:
- A `tag` to identify the migration.
- The `transferred_classes` array, which contains objects with `from`, `from_script`, and `to` properties.
- `from` property is the name of the source Durable Object class.
- `from_script` property is the name of the source Worker script.
- `to` property is the name of the destination Durable Object class.
- Ensure you reference the name of the new, destination Durable Object class in your Worker code.
- Deploy the Worker.
You can transfer stored Durable Objects from DurableObjectExample to TransferredClass from a Worker script named OldWorkerScript. The configuration of the Wrangler configuration file for your new Worker code (destination Worker code) would look like this:
Migration Wrangler configuration
-
Migrations are performed through the
[[migrations]]configurations key in yourwrangler.tomlfile ormigrationskey in yourwrangler.jsoncfile. -
Migrations require a migration tag, which is defined by the
tagproperty in each migration entry. -
Migration tags are treated like unique names and are used to determine which migrations have already been applied. Once a given Worker code has a migration tag set on it, all future Worker code deployments must include a migration tag.
-
The migration list is an ordered array of tables, specified as a key in your Wrangler configuration file.
-
You can define the migration for each environment, as well as at the top level.
- Top-level migration is specified at the top-level
migrationskey in the Wrangler configuration file. - Environment-level migration is specified by a
migrationskey inside theenvkey of the Wrangler configuration file ([env.<environment_name>.migrations]).- Example Wrangler file:
{ // top-level default migrations "migrations": [ { "tag": "v1", "new_sqlite_classes": ["MyDurableObject"] }, ], "env": { "staging": { // migration override for staging "migrations": [ { "tag": "v1-staging", "new_sqlite_classes": ["MyDurableObject"] }, ], }, }, } - If a migration is only specified at the top-level, but not at the environment-level, the environment will inherit the top-level migration.
- Migrations at the environment-level override migrations at the top level.
- Top-level migration is specified at the top-level
-
All migrations are applied at deployment. Each migration can only be applied once per environment.
-
Each migration in the list can have multiple directives, and multiple migrations can be specified as your project grows in complexity.
:::caution[Important]
-
The destination class (the class that stored Durable Objects are being transferred to) for a Rename or Transfer migration must be exported by the deployed Worker.
-
You should not create the destination Durable Object class before running a Rename or Transfer migration. The migration will create the destination class for you.
-
After a Rename or Transfer migration, requests to the destination Durable Object class will have access to the source Durable Object's stored data.
-
After a migration, any existing bindings to the original Durable Object class (for example, from other Workers) will automatically forward to the updated destination class. However, any Workers bound to the updated Durable Object class must update their Durable Object binding configuration in the
wranglerconfiguration file for their next deployment.
:::
:::note
.toml files do not allow line breaks in inline tables (the {key = "value"} syntax), but line breaks in the surrounding inline array are acceptable.
:::
You cannot enable a SQLite storage backend on an existing, deployed Durable Object class, so setting new_sqlite_classes on later migrations will fail with an error. Automatic migration of deployed classes from their key-value storage backend to SQLite storage backend will be available in the future.
:::caution[Important] Durable Object migrations are atomic operations and cannot be gradually deployed. To provide early feedback to developers, new Worker versions with new migrations cannot be uploaded. Refer to Gradual deployments with Durable Objects for more information. :::
Durable Object class exports
import { Render, GlossaryTooltip, WranglerConfig, Steps, Details, Type, MetaInfo, } from "~/components";
The exports field in your Wrangler configuration file is the declarative way to manage Durable Object class lifecycle. You declare each Durable Object class your Worker exports — along with whether it is live, deleted, renamed, or transferred — and Cloudflare reconciles your declaration against the namespaces that have already been provisioned for your Worker.
This page covers how to:
- Create a new Durable Object class.
- Delete a Durable Object class and its data.
- Rename a Durable Object class.
- Transfer a Durable Object class between Workers.
:::note[Looking for the legacy migrations array?]
The exports field replaces the imperative migrations array used by older Workers. Both flows are supported, but a Worker can only use one at a time. To move an existing Worker from migrations to exports, refer to Migrate from the legacy migrations flow.
:::
How exports works
When you deploy a Worker that declares exports, Cloudflare compares three sources of truth:
- Your code — the set of Durable Object classes the Worker actually exports.
- Your
exportsconfiguration — what you have declared about each class. - Provisioned state — the Durable Object namespaces that already exist for this Worker.
A class that appears only in your code is ignored until you declare it in exports; Cloudflare does not provision a namespace implicitly. Other disagreements, such as a live entry whose class is missing from code or a provisioned namespace with no matching entry, are surfaced as structured errors or, where the intent is unambiguous, actions that Cloudflare applies for you.
A minimal exports block declares each Durable Object class as a live entry:
{
"durable_objects": {
"bindings": [
{
"name": "MY_DURABLE_OBJECT",
"class_name": "MyDurableObject",
},
],
},
"exports": {
"MyDurableObject": {
"type": "durable-object",
"storage": "sqlite",
},
},
}
To declare a destructive operation — deleting, renaming, or transferring a class — you change the entry's state to a tombstone variant. The class name remains the same; the value tells Cloudflare what to do with the existing namespace.
| Operation | state |
Required fields |
|---|---|---|
| Define a new class (default) | "created" (or omitted) |
storage |
| Delete a class | "deleted" |
(none) |
| Rename a class | "renamed" |
renamed_to |
| Transfer a class to another Worker | "transferred" |
transferred_to |
| Receive a transfer from another Worker | "expecting-transfer" |
storage, transfer_from |
The rest of this page describes each operation in detail and links to the exports configuration reference for the full schema.
Define a Durable Object class
To define a new Durable Object class, add an entry to exports keyed by the class name and set storage to "sqlite".
```ts title="src/index.ts"
import { DurableObject } from "cloudflare:workers";
export class MyDurableObject extends DurableObject {
// ...
}
```
-
Add a binding for the class (if your Worker needs to access it through
```jsonc { "durable_objects": { "bindings": [ { "name": "MY_DURABLE_OBJECT", "class_name": "MyDurableObject" } ] }, "exports": { "MyDurableObject": { "type": "durable-object", "storage": "sqlite" } } } ```env) and declare the class inexports: -
Deploy the Worker:
npx wrangler deploy
Cloudflare provisions a namespace for the class the first time you deploy. On subsequent deploys with the same entry, no namespace changes are made — the entry simply confirms that the class is still live.
:::note
storage is required for live entries. New Durable Object namespaces created through exports always use the SQLite storage backend. The legacy-kv value is only accepted for namespaces that were already provisioned with key-value storage; refer to Storage backends.
:::
Delete a Durable Object class
Replace the live entry with a deleted tombstone to retire a Durable Object class. Deleting a class removes its namespace and all of its stored data permanently — this is not a soft delete.
<WranglerConfig>
```jsonc
{
"exports": {
"OldDurableObject": {
"type": "durable-object",
"state": "deleted"
}
}
}
```
</WranglerConfig>
-
Deploy the Worker:
npx wrangler deploy
:::caution[Deleting a class destroys its data]
There is no Trash for Durable Object namespaces deleted through exports. Copy any data you need to keep to another location before deploying a deleted tombstone.
:::
A deleted tombstone has two preconditions enforced at deploy time:
- The class must not be present in your Worker code. Cloudflare will not delete a namespace whose class is still being shipped, because runtime bindings would resolve to a deleted namespace.
- No other Worker in your account may bind to the namespace. If another Worker still binds to the class, the deploy is rejected with
tombstone_delete_blocked_by_external_bindingsand the list of referencing scripts is returned. Redeploy those Workers without the binding first, then re-run your deploy.
Once the namespace has been deleted, the tombstone becomes stale. Cloudflare reports this in the reconciliation output and lists the entry in removable_entries so you can safely remove it from exports.
Rename a Durable Object class
Renaming a Durable Object class moves stored data from one class to another within the same Worker. Renaming requires:
- A
renamedtombstone keyed by the old class name, withrenamed_topointing at the new class name. - A live entry for the new class name in the same
exportsmap (so the data has somewhere to land).
For a brand-new deploy where the old class is no longer in your code, a single deploy is enough:
```jsonc { "exports": { "OldName": { "type": "durable-object", "state": "renamed", "renamed_to": "NewName" }, "NewName": { "type": "durable-object", "storage": "sqlite" } } } ```After the rename applies, the namespace's class name is updated to NewName and runtime bindings that reference the new class name resolve to the same data.
Avoid downtime during a rename
A Durable Object class rename involves two updates that are not perfectly atomic at the runtime layer: the namespace's class pointer and the Worker code that exports the class. During a deploy rollout, one update may be visible before the other for a few seconds. To avoid runtime errors during this window, use a three-deploy rename:
1. **Alias the new name to the old class.** Add the new class name to your code as the canonical class, and re-export it under the old name so existing instances keep resolving:```ts title="src/index.ts"
import { DurableObject } from "cloudflare:workers";
export class NewName extends DurableObject {
// ...
}
export { NewName as OldName };
```
Leave `exports` unchanged. Deploy.
-
Apply the rename while the alias is still in place. Update
```jsonc { "exports": { "OldName": { "type": "durable-object", "state": "renamed", "renamed_to": "NewName" }, "NewName": { "type": "durable-object", "storage": "sqlite" } } } ```exportsto add therenamedtombstone and the new live entry:Deploy. Cloudflare applies the rename and surfaces a
tombstone_class_still_in_codeinfo notice — this is expected and confirms the safe rollout pattern. -
Remove the alias. Once the previous deploy has fully rolled out, remove the
OldNamealias from your code:import { DurableObject } from "cloudflare:workers"; export class NewName extends DurableObject { // ... }Leave
exportsunchanged. Deploy. TheOldNametombstone is now stale and Cloudflare lists it inremovable_entries— you can delete the entry fromexportsin your next config edit.
The renamed_to target must:
- Be a valid JavaScript identifier and differ from the source class name.
- Appear as a live entry (
state: "created"or omitted) in the sameexportsmap. Cloudflare rejects arenamed_tovalue that names another tombstone or is missing entirely. - Not collide with an existing namespace under the same name on this Worker. If a namespace already exists under the target name, delete it via its own
deletedtombstone in a prior deploy first.
Transfer a Durable Object class between Workers
Transferring moves an existing Durable Object namespace from one Worker (the source) to another Worker (the target) in the same account. Because two Workers must coordinate, transfer is a multi-deploy flow.
The target Worker declares an expecting-transfer entry that names the source Worker. The source Worker declares a transferred tombstone that names the target Worker. The actual handoff commits when the source Worker's deploy lands.
The recommended sequence is four deploys:
1. **Source Worker — initial state.** The source declares `MyDO` as a live class. Nothing else has changed yet.<WranglerConfig>
```jsonc title="source Worker"
{
"exports": {
"MyDO": { "type": "durable-object", "storage": "sqlite" }
}
}
```
</WranglerConfig>
-
Target Worker — receive the transfer. The target adds
```jsonc title="target Worker" { "exports": { "MyDO": { "type": "durable-object", "state": "expecting-transfer", "storage": "sqlite", "transfer_from": "source-worker" } } } ```MyDOto its code and declares anexpecting-transferentry naming the source Worker. Do not add adurable_objects.bindingsentry forMyDOon the target yet — Cloudflare does not route self-referencing bindings through the source's namespace during this phase.Deploy the target. Cloudflare records a pending transfer and emits a
Transfer pendingnotice in the reconciliation output. -
Source Worker — commit the transfer. Change the source's
```jsonc title="source Worker" { "exports": { "MyDO": { "type": "durable-object", "state": "transferred", "transferred_to": "target-worker" } } } ```MyDOentry to atransferredtombstone naming the target Worker. Leave the class in the source code for now:Deploy the source. Cloudflare matches the pending transfer and atomically reassigns the namespace to the target Worker. The reconciliation output reports
Transferred (committed): MyDO → target-worker.If the source Worker still needs to access
```jsonc title="source Worker" { "durable_objects": { "bindings": [ { "name": "MY_DURABLE_OBJECT", "class_name": "MyDO", "script_name": "target-worker" } ] } } ```MyDOafter the transfer commits, update itsdurable_objects.bindingsentry to point at the target Worker withscript_name:If the source Worker no longer needs to access
MyDO, remove the binding when you removeMyDOfrom the source Worker's code. -
Target Worker — bind the class. Once the source's deploy has fully rolled out, redeploy the target with a
```jsonc title="target Worker" { "durable_objects": { "bindings": [ { "name": "MY_DURABLE_OBJECT", "class_name": "MyDO" } ] }, "exports": { "MyDO": { "type": "durable-object", "storage": "sqlite" } } } ```durable_objects.bindingsentry forMyDO. The binding now resolves to the namespace on the target Worker.
After the handoff has rolled out everywhere, you can remove MyDO from the source Worker's code and delete the transferred tombstone from the source's exports map. While other Workers in the account still bind to MyDO on the source, the reconciliation output lists them in referencing_scripts; redeploy each of them with bindings re-pointed at the target before removing the source tombstone.
Cancel a pending transfer
A pending transfer persists until the source Worker commits with a transferred tombstone, or the target Worker cancels by removing the expecting-transfer entry. To cancel, redeploy the target without the entry (or replace it with a normal live entry). Cloudflare deletes the pending record and the source Worker keeps its namespace.
Transfer constraints
- Both Workers must live in the same Cloudflare account.
- Cross-dispatch-namespace transfers are not supported. The source and target Workers must both be in the same dispatch-namespace context (or both be outside any dispatch namespace).
- A target Worker can hold only one pending phase-1 hint per class at a time. To redirect a pending transfer to a different source, cancel the current pending transfer first.
Storage backends
Live entries (state: "created" and state: "expecting-transfer") must declare a storage value:
"sqlite"selects the SQLite storage backend. This is the recommended and only path for new namespaces. SQLite-backed namespaces support SQL, Point-in-Time Recovery, and a higher per-object storage limit."legacy-kv"selects the key-value storage backend. Cloudflare only accepts this value for namespaces that were already provisioned with key-value storage (typically Workers that started on the legacymigrationsarray before SQLite was the default). You cannot create a new key-value-backed namespace throughexports.
Storage type is immutable once a namespace exists. Cloudflare rejects an exports change that switches a provisioned namespace from sqlite to legacy-kv or vice versa with the error storage_type_mismatch. To genuinely change storage backends, delete the namespace via a deleted tombstone and re-provision it under the new backend (with full data loss in between).
Reading the reconciliation output
When a deployment applies a Durable Object class lifecycle change, wrangler deploy prints a Durable Object exports reconciliation block. The block also appears when reconciliation produces warnings, info notices, or removable entries. Wrangler omits the block when nothing changed and there are no notices.
A typical output looks like:
Durable Object exports reconciliation:
Created: ChatRoom
Renamed: OldRoom → ChatRoom
Transferred (committed): Widget → target-worker
Transfer pending: Incoming ← source-worker
Info:
[tombstone_class_still_in_code] OldRoom: Tombstone of type 'renamed' applied. Class 'OldRoom' is still exported in code; this is the supported pattern for zero-downtime rename rollouts.
[stale_tombstone] OldGone: Tombstone of type 'deleted' for class 'OldGone' has no effect (no namespace exists with this class name). Safe to remove from `exports`.
Safe to remove from `exports`: OldGone
The block has four sections:
- Action lines (
Created,Updated,Deleted,Renamed,Transferred,Transfer pending) report the changes Cloudflare applied during this deploy. - Warnings (yellow) flag conditions that should be investigated but do not block the deploy. The set of warning scenarios is reserved for future use; current Cloudflare deploys do not emit them.
- Info (dimmed) reports non-blocking notices: stale tombstones that no longer apply, and tombstones applied while the source class is still in code (the supported pattern for zero-downtime rollouts).
- Safe to remove from
exportslists tombstone entries that are stale and have no other Workers in the account binding to the source class. You can delete these entries from yourexportsmap on your next config edit.
Info entries can include a referencing_scripts list — other Workers in the account whose bindings still resolve to the affected namespace. Redeploy those Workers with bindings re-pointed at the new class name before removing the tombstone, or you will orphan their bindings.
If the deploy fails, the output shows a structured error per class:
✘ [orphaned_provisioned_namespace] class 'Bar': A namespace exists for 'Bar' but no `exports` entry declares it.
Suggestion: add an `exports` entry for 'Bar', or add a `deleted` tombstone to remove the namespace.
Referencing scripts: worker-foo, worker-bar
All class-level errors from a single deploy are reported together so you can fix them in one round trip. Refer to the error reference for the full list of error scenarios.
Stale tombstones and cleanup
A tombstone becomes stale when applying it produces no state change — for example, a deleted tombstone for a class whose namespace has already been removed, or a renamed tombstone after the rename has already landed.
Cloudflare emits a stale_tombstone info notice on every deploy where a tombstone is stale, until you remove the entry from exports. The notice is intentionally repeated so you do not forget to clean up.
For renamed and transferred tombstones, Cloudflare also enumerates other Workers in your account whose durable_objects.bindings entries still reference the source class name. These appear as referencing_scripts on the info notice. While referencing_scripts is non-empty, removing the tombstone could orphan those bindings — redeploy the referencing Workers with bindings re-pointed at the new class first.
The top-level "Safe to remove from exports" line in the reconciliation output names every stale tombstone whose referencing_scripts is empty. Use that list as the authoritative "you can delete these now" hint.
Environments, dispatch namespaces, and previews
exports can be specified at the top level of your Wrangler configuration file and overridden per environment:
If exports is only declared at the top level, named environments inherit the same value. Each environment maintains its own provisioned namespaces, so tombstones apply only within the environment they are declared in.
Previews and dispatch namespaces follow the same rules. Cross-dispatch-namespace transfers are not supported — the source and target Workers in a transferred / expecting-transfer pair must both live in the same dispatch-namespace context (or both be outside any dispatch namespace).
exports configuration reference
The exports field is a map keyed by Durable Object class name. Each value is an object whose fields depend on state:
-
type- For Durable Object class entries, set this to
"durable-object".
- For Durable Object class entries, set this to
-
state- The lifecycle state. One of
"created"(the default, omit to use),"deleted","renamed","transferred", or"expecting-transfer".
- The lifecycle state. One of
-
storage- Required when
stateis"created"or"expecting-transfer". One of"sqlite"(recommended, the only valid value for new namespaces) or"legacy-kv"(only for existing key-value-backed namespaces). Forbidden on tombstone states.
- Required when
-
renamed_to- Required when
stateis"renamed". The destination class name. Must be a valid JavaScript identifier, differ from the source class name, and appear as a live entry in the sameexportsmap.
- Required when
-
transferred_to- Required when
stateis"transferred". The name of the target Worker that will receive the namespace.
- Required when
-
transfer_from- Required when
stateis"expecting-transfer". The name of the source Worker the namespace is being transferred from.
- Required when
The following table lists the allowed and forbidden fields for each state:
state |
Required | Forbidden |
|---|---|---|
"created" (default) |
storage |
renamed_to, transferred_to, transfer_from |
"deleted" |
(none) | storage, renamed_to, transferred_to, transfer_from |
"renamed" |
renamed_to |
storage, transferred_to, transfer_from |
"transferred" |
transferred_to |
storage, renamed_to, transfer_from |
"expecting-transfer" |
storage, transfer_from |
renamed_to, transferred_to |
Error reference
When a deploy fails reconciliation, Cloudflare returns one error per class along with a structured scenario tag, a human-readable message, and where applicable a suggestion and referencing_scripts list:
| Scenario | Meaning | How to fix |
|---|---|---|
provisioned_class_missing_from_config |
A namespace exists for a class that the Worker code still exports, but exports has no entry for it. |
Add a live entry (state: "created") to keep the namespace, or add a tombstone (deleted, renamed, or transferred) to retire it. |
config_export_not_in_code |
A live entry declares a class that the Worker code does not export. | Add the class to your code, or replace the entry with a tombstone. |
config_references_nonexistent_class |
A live entry declares a class that is neither in code nor provisioned. | Remove the entry, or add the class to your Worker code. |
orphaned_provisioned_namespace |
A namespace exists for a class that is neither in code nor declared. | Add a tombstone for the class, or add the class back to code and exports. |
invalid_export |
An exports entry is structurally invalid — for example, requesting legacy-kv for a new namespace, or a class that maps to more than one provisioned namespace. |
Correct the entry: use "storage": "sqlite" for new namespaces, or resolve the duplicate with a deleted or renamed tombstone. |
tombstone_delete_class_still_in_code |
A deleted tombstone names a class that is still exported in code. |
Remove the class from your code first, then deploy the tombstone. |
tombstone_delete_blocked_by_external_bindings |
Another Worker in the account binds to the namespace being deleted. | Redeploy the referencing Workers without the binding, then re-run your deploy. |
tombstone_renamed_to_occupied |
A renamed_to target collides with an existing namespace under that name. |
Delete the colliding namespace via its own deleted tombstone in a prior deploy. |
transferred_pending_not_found |
A transferred tombstone has no matching expecting-transfer entry on the target. |
Deploy the target Worker first with an expecting-transfer entry naming this Worker. |
transferred_target_missing |
The target Worker named by transferred_to no longer exists. |
Update transferred_to to a valid target, or remove the tombstone. |
transferred_target_mismatch |
The transferred_to value does not match the target recorded by the pending transfer. |
Set transferred_to to the pending transfer's target, or have the target cancel its expecting-transfer entry before redirecting. |
transferred_source_in_dispatch_namespace |
The source Worker declaring the transferred tombstone is in a dispatch namespace; cross-dispatch transfer is not supported. |
Perform the transfer within a single dispatch-namespace context. |
transferred_target_in_dispatch_namespace |
The target of a transferred tombstone is in a dispatch namespace; cross-dispatch transfer is not supported. |
Cancel the pending transfer by having the target redeploy without expecting-transfer, or keep both Workers in the same context. |
phase_one_transfer_after_commit_mismatch |
The target's namespace was transferred from a different source than the one declared. | Remove the expecting-transfer entry — the transfer cannot be redirected after commit. |
phase_one_transfer_target_class_provisioned |
The target Worker already owns a namespace for the class. | Replace the expecting-transfer entry with a normal live entry, or delete the existing namespace first. |
phase_one_transfer_duplicate |
Another expecting-transfer hint is already in flight for the same class. |
Cancel the existing pending transfer first by removing or replacing its entry. |
phase_one_transfer_source_missing |
The source Worker named by transfer_from does not exist in the account. |
Correct the source Worker name. |
phase_one_transfer_source_namespace_missing |
The source Worker has no namespace for the class. | Make sure the source Worker has the class deployed before the target declares expecting-transfer. |
phase_one_transfer_source_in_dispatch_namespace |
The source Worker is in a dispatch namespace; cross-dispatch transfer is not supported. | Move the transfer within a single dispatch-namespace context. |
phase_one_transfer_target_in_dispatch_namespace |
The target Worker is in a dispatch namespace; cross-dispatch transfer is not supported. | Move the transfer within a single dispatch-namespace context. |
storage_type_mismatch |
The declared storage value does not match the provisioned namespace's storage backend. |
Storage backends cannot be changed in place. Delete the namespace and re-provision it under the new backend if you genuinely need to switch. |
free_tier_requires_sqlite |
The account's plan only supports SQLite-backed namespaces, but the entry requests legacy-kv. |
Use "storage": "sqlite". |
:::note
The blocking error tombstone_delete_class_still_in_code (specific to deleted tombstones) is distinct from the non-blocking tombstone_class_still_in_code info notice emitted for renamed and transferred tombstones. The latter is the expected zero-downtime rollout pattern — refer to Reading the reconciliation output.
:::
Constraints and limitations
- Durable Object entries in
exportsare mutually exclusive withmigrations. A Worker configuration that contains both is rejected at validation. Workflow entries inexportscan be used alongsidemigrations. Once a Worker has been deployed withexports, subsequent deploys must continue to useexports(or neither, which reconciles against an empty config and is usually a mistake). wrangler versions uploaddoes not apply lifecycle changes. Just like the legacymigrationsarray, Durable Object lifecycle changes can only be applied viawrangler deploy. If your Wrangler configuration containsexportsentries,wrangler versions uploadfails fast with an actionable error. Refer to Deployment management - Durable Object migrations.- Gradual deployments are not supported with
exports. Lifecycle changes are atomic at the Cloudflare control plane and cannot be rolled out gradually. Refer to Gradual deployments with Durable Objects - Durable Object class lifecycle changes. - Rollbacks cannot cross a lifecycle change. You cannot roll back to a version deployed before an
exports-driven lifecycle change. Refer to Rollbacks - Bindings. - Storage backends are immutable once provisioned. You cannot change a namespace's
storagevalue in place; delete and re-provision instead.
Migrate from the legacy migrations flow
Existing Workers using the migrations array can move to exports without any data migration. The provisioned namespaces remain in place; only the configuration shape changes.
To determine a class's existing storage backend, trace it to the migration that originally created it. Classes introduced through new_sqlite_classes use sqlite, while classes introduced through new_classes use legacy-kv.
Side-by-side, the equivalent of a typical migrations history is:
Future lifecycle changes (deletes, renames, transfers) happen entirely through exports — there is no equivalent of the tag field and no need to keep historical entries. The current state of your exports map is the source of truth.
:::caution
Once a Worker has been deployed with exports, subsequent deploys cannot return to the legacy migrations array. Plan your transition accordingly.
:::
Environments
import { WranglerConfig, DashButton } from "~/components";
Environments provide isolated spaces where your code runs with specific dependencies and configurations. This can be useful for a number of reasons, such as compatibility testing or version management. Using different environments can help with code consistency, testing, and production segregation, which reduces the risk of errors when deploying code.
Wrangler environments
Wrangler allows you to deploy the same Worker application with different configuration for each environment.
If you are using Wrangler environments, you must specify any Durable Object bindings you wish to use on a per-environment basis.
Durable Object bindings are not inherited. For example, you can define an environment named staging as below:
{
"env": {
"staging": {
"durable_objects": {
"bindings": [
{
"name": "EXAMPLE_CLASS",
"class_name": "DurableObjectExample"
}
]
}
}
}
}
Because Wrangler appends the environment name to the top-level name when publishing, for a Worker named worker-name the above example is equivalent to:
{
"env": {
"staging": {
"durable_objects": {
"bindings": [
{
"name": "EXAMPLE_CLASS",
"class_name": "DurableObjectExample",
"script_name": "worker-name-staging"
}
]
}
}
}
}
"EXAMPLE_CLASS" in the staging environment is bound to a different Worker code name compared to the top-level "EXAMPLE_CLASS" binding, and will therefore access different Durable Objects with different persistent storage.
If you want an environment-specific binding that accesses the same Objects as the top-level binding, specify the top-level Worker code name explicitly using script_name:
{
"env": {
"another": {
"durable_objects": {
"bindings": [
{
"name": "EXAMPLE_CLASS",
"class_name": "DurableObjectExample",
"script_name": "worker-name"
}
]
}
}
}
}
Migration environments
You can define a Durable Object migration for each environment, as well as at the top level. Migrations at the environment-level override migrations at the top level.
For more information, refer to Migration Wrangler Configuration.
Local development
Local development sessions create a standalone, local-only environment that mirrors the production environment, so that you can test your Worker and Durable Objects before you deploy to production.
An existing Durable Object binding of DB would be available to your Worker when running locally.
Refer to Workers Local development.
Remote development
KV-backed Durable Objects support remote development using the dashboard playground. The dashboard playground uses a browser version of Visual Studio Code, allowing you to rapidly iterate on your Worker entirely in your browser.
To start remote development:
-
In the Cloudflare dashboard, go to the Workers & Pages page.
-
Select an existing Worker.
-
Select the Edit code icon located on the upper-right of the screen.
:::caution Remote development is only available for KV-backed Durable Objects. SQLite-backed Durable Objects do not support remote development. :::
FAQs
import { Render } from "~/components";
Pricing
Limits
Metrics and analytics
Glossary
import { Glossary } from "~/components"
Review the definitions for terms used across Cloudflare's Durable Objects documentation.
In-memory state in a Durable Object
import { GlossaryTooltip } from "~/components";
In-memory state means that each Durable Object has one active instance at any particular time. All requests sent to that Durable Object are handled by that same instance. You can store some state in memory.
Variables in a Durable Object will maintain state as long as your Durable Object is not evicted from memory.
A common pattern is to initialize a Durable Object from persistent storage and set instance variables the first time it is accessed. Since future accesses are routed to the same Durable Object, it is then possible to return any initialized values without making further calls to persistent storage.
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
// `blockConcurrencyWhile()` ensures no requests are delivered until
// initialization completes.
this.ctx.blockConcurrencyWhile(async () => {
let stored = await this.ctx.storage.get("value");
// After initialization, future reads do not need to access storage.
this.value = stored || 0;
});
}
// Handle HTTP requests from clients.
async fetch(request) {
// use this.value rather than storage
}
}
A given instance of a Durable Object may share global memory with other instances defined in the same Worker code.
In the example above, using a global variable value instead of the instance variable this.value would be incorrect. Two different instances of Counter will each have their own separate memory for this.value, but might share memory for the global variable value, leading to unexpected results. Because of this, it is best to avoid global variables.
:::note[Built-in caching]
The Durable Object's storage has a built-in in-memory cache of its own. If you use get() to retrieve a value that was read or written recently, the result will be instantly returned from cache. Instead of writing initialization code like above, you could use get("value") whenever you need it, and rely on the built-in cache to make this fast. Refer to the Build a counter example to learn more about this approach.
However, in applications with more complex state, explicitly storing state in your Object may be easier than making Storage API calls on every access. Depending on the configuration of your project, write your code in the way that is easiest for you.
:::
:::note[Observe in-memory state size]
You can monitor V8 isolate memory usage — which includes the in-memory state your Durable Objects hold — with the Memory usage chart on the Metrics tab. Memory is measured per isolate (which can host multiple Durable Objects), not per object. Refer to Metrics and analytics for details, including how filtering by Durable Object ID or name affects what the chart shows. Because in-memory state is not preserved across eviction or hibernation, persist anything important to storage.
:::
Reference
import { DirectoryListing } from "~/components";
Build a seat booking app with SQLite in Durable Objects
import { Render, PackageManagers, Details, WranglerConfig } from "~/components";
In this tutorial, you will learn how to build a seat reservation app using Durable Objects. This app will allow users to book a seat for a flight. The app will be written in TypeScript and will use the new SQLite storage backend in Durable Object to store the data.
Using Durable Objects, you can write reusable code that can handle coordination and state management for multiple clients. Moreover, writing data to SQLite in Durable Objects is synchronous and uses local disks, therefore all queries are executed with great performance. You can learn more about SQLite storage in Durable Objects in the SQLite in Durable Objects blog post.
:::note[SQLite in Durable Objects]
SQLite in Durable Objects is currently in beta. You can learn more about the limitations of SQLite in Durable Objects in the SQLite in Durable Objects documentation.
:::
The application will function as follows:
Let's get started!
Prerequisites
- Sign up for a Cloudflare account.
- Install
Node.js.
For supported Node.js versions, refer to the
[Wrangler system requirements](/workers/wrangler/install-and-update/#install-wrangler).
1. Create a new project
Create a new Worker project to create and deploy your app.
-
Create a Worker named
seat-bookingby running:<PackageManagers type="create" pkg="cloudflare@latest" args={"seat-booking"} />
<Render file="c3-post-run-steps-hello-world" product="workers" params={{ type: "Worker + Durable Objects", lang: "TypeScript", }} />
-
Change into your new project directory to start developing:
cd seat-booking
2. Create the frontend
The frontend of the application is a simple HTML page that allows users to select a seat and enter their name. The application uses Workers Static Assets to serve the frontend.
-
Create a new directory named
publicin the project root. -
Create a new file named
index.htmlin thepublicdirectory. -
Add the following HTML code to the
index.htmlfile:
```html title="public/index.html"
<script>
const seatGrid = document.getElementById("seatGrid");
const title = document.getElementById("title");
const flightId = window.location.search.split("=")[1];
const hostname = window.location.hostname;
if (flightId === undefined) {
title.textContent = "No Flight ID provided";
seatGrid.innerHTML = "<p>Add `flightId` to the query string</p>";
} else {
handleBooking();
}
function handleBooking() {
let ws;
if (hostname === 'localhost') {
const port = window.location.port;
ws = new WebSocket(`ws://${hostname}:${port}/ws?flightId=${flightId}`);
} else {
ws = new WebSocket(`wss://${hostname}/ws?flightId=${flightId}`);
}
title.textContent = `Book seat for flight ${flightId}`;
ws.onopen = () => {
console.log("Connected to WebSocket server");
};
function createSeatGrid(seats) {
seatGrid.innerHTML = "";
for (let row = 1; row <= 10; row++) {
for (let col = 0; col < 6; col++) {
if (col === 3) {
const aisle = document.createElement("div");
aisle.className = "aisle";
seatGrid.appendChild(aisle);
}
const seatNumber = `${row}${String.fromCharCode(65 + col)}`;
const seat = seats.find((s) => s.seatNumber === seatNumber);
const seatElement = document.createElement("div");
seatElement.className = `seat ${seat && seat.occupant ? "unavailable" : "available"}`;
seatElement.textContent = seatNumber;
seatElement.onclick = () => bookSeat(seatNumber);
seatGrid.appendChild(seatElement);
}
}
}
async function fetchSeats() {
const response = await fetch(`/seats?flightId=${flightId}`);
const seats = await response.json();
createSeatGrid(seats);
}
async function bookSeat(seatNumber) {
const name = prompt("Please enter your name:");
if (!name) {
return; // User canceled the prompt
}
const response = await fetch(`book-seat?flightId=${flightId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ seatNumber, name }),
});
const result = await response.text();
fetchSeats();
}
ws.onmessage = (event) => {
try {
const seats = JSON.parse(event.data);
createSeatGrid(seats);
} catch (error) {
console.error("Error parsing WebSocket message:", error);
}
};
ws.onerror = (error) => {
console.error("WebSocket error:", error);
};
ws.onclose = (event) => {
console.log("WebSocket connection closed:", event);
};
fetchSeats();
}
</script>
</body>
```
- The frontend makes an HTTP
GETrequest to the/seatsendpoint to retrieve the available seats for the flight. - It also uses a WebSocket connection to receive updates about the available seats.
- When a user clicks on a seat, the
bookSeat()function is called that prompts the user to enter their name and then makes aPOSTrequest to the/book-seatendpoint.
- Update the bindings in the Wrangler configuration file to configure
assetsto serve thepublicdirectory.
{
"assets": {
"directory": "public"
}
}
- If you start the development server using the following command, the frontend will be served at
http://localhost:8787. However, it will not work because the backend is not yet implemented.
npm run dev
:::note[Workers Static Assets]
Workers Static Assets is currently in beta. You can also use Cloudflare Pages to serve the frontend. However, you will need a separate Worker for the backend.
:::
3. Create table for each flight
The application already has the binding for the Durable Objects class configured in the Wrangler configuration file. If you update the name of the Durable Objects class in src/index.ts, make sure to also update the binding in the Wrangler configuration file.
- Update the binding to use the SQLite storage in Durable Objects. In the Wrangler configuration file, replace
new_classes=["Flight"]withnew_sqlite_classes=["Flight"],name = "FLIGHT"withname = "FLIGHT", andclass_name = "MyDurableObject"withclass_name = "Flight". your Wrangler configuration file should look similar to this:
{
"durable_objects": {
"bindings": [
{
"name": "FLIGHT",
"class_name": "Flight"
}
]
},
// Durable Object migrations.
// Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#migrations
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"Flight"
]
}
]
}
Your application can now use the SQLite storage in Durable Objects.
- Add the
initializeSeats()function to theFlightclass. This function will be called when the Durable Object is initialized. It will check if the table exists, and if not, it will create it. It will also insert seats information in the table.
For this tutorial, the function creates an identical seating plan for all the flights. However, in production, you would want to update this function to insert seats based on the flight type.
Replace the Flight class with the following code:
import { DurableObject } from "cloudflare:workers";
export class Flight extends DurableObject {
sql = this.ctx.storage.sql;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.initializeSeats();
}
private initializeSeats() {
const cursor = this.sql.exec(`PRAGMA table_list`);
// Check if a table exists.
if ([...cursor].find((t) => t.name === "seats")) {
console.log("Table already exists");
return;
}
this.sql.exec(`
CREATE TABLE IF NOT EXISTS seats (
seatId TEXT PRIMARY KEY,
occupant TEXT
)
`);
// For this demo, we populate the table with 60 seats.
// Since SQLite in DOs is fast, we can do a query per INSERT instead of batching them in a transaction.
for (let row = 1; row <= 10; row++) {
for (let col = 0; col < 6; col++) {
const seatNumber = `${row}${String.fromCharCode(65 + col)}`;
this.sql.exec(`INSERT INTO seats VALUES (?, null)`, seatNumber);
}
}
}
}
- Add a
fetchhandler to theFlightclass. This handler will return a text response. In Step 5 You will update thefetchhandler to handle the WebSocket connection.
import { DurableObject } from "cloudflare:workers";
export class Flight extends DurableObject {
...
async fetch(request: Request): Promise<Response> {
return new Response("Hello from Durable Object!", { status: 200 });
}
}
- Next, update the Worker's fetch handler to create a unique Durable Object for each flight.
export default {
async fetch(request, env, ctx): Promise<Response> {
// Get flight id from the query parameter
const url = new URL(request.url);
const flightId = url.searchParams.get("flightId");
if (!flightId) {
return new Response(
"Flight ID not found. Provide flightId in the query parameter",
{ status: 404 },
);
}
const stub = env.FLIGHT.getByName(flightId);
return stub.fetch(request);
},
} satisfies ExportedHandler<Env>;
Using the flight ID, from the query parameter, a unique Durable Object is created. This Durable Object is initialized with a table if it does not exist.
4. Add methods to the Durable Object
- Add the
getSeats()function to theFlightclass. This function returns all the seats in the table.
import { DurableObject } from "cloudflare:workers";
export class Flight extends DurableObject {
...
private initializeSeats() {
...
}
// Get all seats.
getSeats() {
let results = [];
// Query returns a cursor.
let cursor = this.sql.exec(`SELECT seatId, occupant FROM seats`);
// Cursors are iterable.
for (let row of cursor) {
// Each row is an object with a property for each column.
results.push({ seatNumber: row.seatId, occupant: row.occupant });
}
return results;
}
}
- Add the
assignSeat()function to theFlightclass. This function will assign a seat to a passenger. It takes the seat number and the passenger name as parameters.
import { DurableObject } from "cloudflare:workers";
export class Flight extends DurableObject {
...
private initializeSeats() {
...
}
// Get all seats.
getSeats() {
...
}
// Assign a seat to a passenger.
assignSeat(seatId: string, occupant: string) {
// Check that seat isn't occupied.
let cursor = this.sql.exec(
`SELECT occupant FROM seats WHERE seatId = ?`,
seatId,
);
let result = cursor.toArray()[0]; // Get the first result from the cursor.
if (!result) {
return {message: 'Seat not available', status: 400 };
}
if (result.occupant !== null) {
return {message: 'Seat not available', status: 400 };
}
// If the occupant is already in a different seat, remove them.
this.sql.exec(
`UPDATE seats SET occupant = null WHERE occupant = ?`,
occupant,
);
// Assign the seat. Note: We don't have to worry that a concurrent request may
// have grabbed the seat between the two queries, because the code is synchronous
// (no `await`s) and the database is private to this Durable Object. Nothing else
// could have changed since we checked that the seat was available earlier!
this.sql.exec(
`UPDATE seats SET occupant = ? WHERE seatId = ?`,
occupant,
seatId,
);
// Broadcast the updated seats.
this.broadcastSeats();
return {message: `Seat ${seatId} booked successfully`, status: 200 };
}
}
The above function uses the broadcastSeats() function to broadcast the updated seats to all the connected clients. In the next section, we will add the broadcastSeats() function.
5. Handle WebSocket connections
All the clients will connect to the Durable Object using WebSockets. The Durable Object will broadcast the updated seats to all the connected clients. This allows the clients to update the UI in real time.
- Add the
handleWebSocket()function to theFlightclass. This function handles the WebSocket connections.
import { DurableObject } from "cloudflare:workers";
export class Flight extends DurableObject {
...
private initializeSeats() {
...
}
// Get all seats.
getSeats() {
...
}
// Assign a seat to a passenger.
assignSeat(seatId: string, occupant: string) {
...
}
private handleWebSocket(request: Request) {
console.log('WebSocket connection requested');
const [client, server] = Object.values(new WebSocketPair());
this.ctx.acceptWebSocket(server);
console.log('WebSocket connection established');
return new Response(null, { status: 101, webSocket: client });
}
}
- Add the
broadcastSeats()function to theFlightclass. This function will broadcast the updated seats to all the connected clients.
import { DurableObject } from "cloudflare:workers";
export class Flight extends DurableObject {
...
private initializeSeats() {
...
}
// Get all seats.
getSeats() {
...
}
// Assign a seat to a passenger.
assignSeat(seatId: string, occupant: string) {
...
}
private handleWebSocket(request: Request) {
...
}
private broadcastSeats() {
this.ctx.getWebSockets().forEach((ws) => ws.send(this.getSeats()));
}
}
- Next, update the
fetchhandler in theFlightclass. This handler will handle all the incoming requests from the Worker and handle the WebSocket connections using thehandleWebSocket()method.
import { DurableObject } from "cloudflare:workers";
export class Flight extends DurableObject {
...
private initializeSeats() {
...
}
// Get all seats.
getSeats() {
...
}
// Assign a seat to a passenger.
assignSeat(seatId: string, occupant: string) {
...
}
private handleWebSocket(request: Request) {
...
}
private broadcastSeats() {
...
}
async fetch(request: Request) {
return this.handleWebSocket(request);
}
}
- Finally, update the
fetchhandler of the Worker.
export default {
...
async fetch(request, env, ctx): Promise<Response> {
// Get flight id from the query parameter
...
if (request.method === "GET" && url.pathname === "/seats") {
return new Response(JSON.stringify(await stub.getSeats()), {
headers: { 'Content-Type': 'application/json' },
});
} else if (request.method === "POST" && url.pathname === "/book-seat") {
const { seatNumber, name } = (await request.json()) as {
seatNumber: string;
name: string;
};
const result = await stub.assignSeat(seatNumber, name);
return new Response(JSON.stringify(result));
} else if (request.headers.get("Upgrade") === "websocket") {
return stub.fetch(request);
}
return new Response("Not found", { status: 404 });
},
} satisfies ExportedHandler<Env>;
The fetch handler in the Worker now calls appropriate Durable Object function to handle the incoming request. If the request is a GET request to /seats, the Worker returns the seats from the Durable Object. If the request is a POST request to /book-seat, the Worker calls the bookSeat method of the Durable Object to assign the seat to the passenger. If the request is a WebSocket connection, the Durable Object handles the WebSocket connection.
6. Test the application
You can test the application locally by running the following command:
npm run dev
This starts a local development server that runs the application. The application is served at http://localhost:8787.
Update the URL with the flight ID as http://localhost:8787?flightId=1234. The application displays the seats for the flight with the ID 1234.
7. Deploy the application
To deploy the application, run the following command:
npm run deploy
⛅️ wrangler 3.78.8
-------------------
🌀 Building list of assets...
🌀 Starting asset upload...
🌀 Found 1 new or modified file to upload. Proceeding with upload...
+ /index.html
Uploaded 1 of 1 assets
✨ Success! Uploaded 1 file (1.93 sec)
Total Upload: 3.45 KiB / gzip: 1.39 KiB
Your worker has access to the following bindings:
- Durable Objects:
- FLIGHT: Flight
Uploaded seat-book (12.12 sec)
Deployed seat-book triggers (5.54 sec)
[DEPLOYED_APP_LINK]
Current Version ID: [BINDING_ID]
Summary
In this tutorial, you have:
- used the SQLite storage backend in Durable Objects to store the seats for a flight.
- created a Durable Object class to manage the seat booking.
- deployed the application to Cloudflare Workers!
The full code for this tutorial is available on GitHub.
Tutorials
import { GlossaryTooltip, ListTutorials } from "~/components";
View tutorials to help you get started with Durable Objects.