The Ultimate Guide to Cloudflare's Durable Objects
An In-Depth Exploration of the Technology That's Redefining Stateful Serverless Apps
If you’ve clicked into this post and wondered “what on earth is a Durable Object?”, then you’re in the right place - and trust me, you’re not alone, the vast majority of the tech community haven’t heard of them either.
And that’s a huge shame, because they are incredibly powerful and, as far as I know, unique to the Cloudflare developer platform. They do take some getting used to, and the aim of this post is to get you familiar with them, in the hope you can unlock the power they can bring.
Introduction
What are Durable Objects?
Let’s start out with the easy question, or so you would think, right? Well, I’ve been working with Cloudflare’s developer platform for many years, and used Durable Objects frequently, and for a long time, I struggled to explain what one was.
However, I feel like I’ve now settled on a consistent definition for them.
Durable Objects allow you to instantly spin up a (near) infinite number of mini servers across the world, with built-in durable storage, that can hibernate between requests.
It’s incredibly difficult to write a one-liner to encapsulate everything a Durable Object can do, but this is the best I’ve come up with. As a teaser for later, they can also:
Provide coordination in multiplayer scenarios (each instance of a Durable Object that you create has a unique ID, and only one instance can exist with that ID globally at any one time)
Built-in WebSocket capabilities (a few lines of code to get going!)
Able to wake up outside of HTTP requests (these are called alarms)
Manage the entire lifecycle of the key entities in your domain, in a way that feels very organic and natural
More recently, are the perfect mechanism to create and expose AI Agents
At its core, a Durable Object looks like just like any other class you define in your code in terms of appearance - more on that later. That’s part of the beauty: you can write what’s effectively “normal” code, and the platform will handle the rest in terms of maintaining any Durable Objects you create, and the networking required to interact with them.
Why Durable Objects Matter
The Cloudflare developer platform is entirely serverless, and Durable Objects are no different, effectively giving you stateful serverless applications. This is actually incredibly rare, when you consider similar (but not really) technologies such as Lambda, that have to use external data storage and retrieve any state when needed.
To fully understand why Durable Objects are revolutionary, we need to take a step back and understand a little more about the Cloudflare developer platform. It’s a global platform, with no region restrictions - when you deploy, you deploy to the entire world.
That might sound unrealistic, but it’s genuinely what happens. If you deploy a Cloudflare Worker, which is the underlying technology that powers Durable Objects, it can be spun up in any of the 300 cities where Cloudflare has data centres. As you can imagine, this will significantly reduce latency to the end user, and give you more redundancy and failover than you could ever need.
The same is true for Durable Objects, but with a twist. With Cloudflare Workers, they can be spun up in as many locations as needed. If you’re handling requests simultaneously from New York, London, Sydney and South Korea, you’ll likely end up with 4 instances of that Worker in each location.
With each instance of a Durable Object, as there can only be one instance of that Durable Object with a given ID, it will be created (by default) close to the location of where the first request originates. It will then live there indefinitely, but it can be moved around depending on availability of servers and other factors, as Cloudflare will maintain the state of your Durable Objects in numerous locations worldwide in case it’s “home” server is down.
This is all very cool, but why does it matter? For the same reasons it matters that a Cloudflare Worker is deployed globally - your Durable Objects can be spun up across the world instantly, and recover from failures in specific data centres. Being able to horizontally scale your application with zero effort is a pretty big win too, if you ask me.
Compare that with a platform like AWS, where your resources are typically confined to a single region, you then have to worry a lot more about that region going down, alongside larger latency when requests come in from afar.
I think this all comes to life a bit more if we look at some use cases, so let’s dive into that next.
Common Use Cases for Durable Objects
This won’t be an exhaustive list, but it should hopefully give you an idea of what’s possible with Durable Objects, and highlight their versatility. Let’s tackle the obvious one first, that is the go-to use case for Durable Objects: multiplayer.
Yes, this could mean some sort of multiplayer game, but these days lots of things in the business world are “multiplayer”. Google Docs for example, or Miro, where changes made by numerous people working on a single document are all synchronised across clients in real-time.
The reason Durable Objects shine at multiplayer is two-fold. Firstly, they come with built-in WebSockets, which are the most common way to implement real-time applications on the web. I always thought WebSockets were difficult to setup, and they can be if you manage the infrastructure yourself, but with Durable Objects, you can have WebSockets working in a few lines of code.
Secondly, because new instances of a Durable Object are created dynamically at runtime, you can create as many as you need on-demand. If a user creates a new document, or a new game lobby, you simply create a new Durable Object, connect all the clients and then you’ve effortlessly sharded your application that will scale horizontally almost to infinity.
That second point is actually another use case on its own, and not tied to multiplayer. Multi-tenancy is an incredibly common pattern in today’s SaaS world, where a large number of users all use the same website or application.
How do you segregate the data safely and ensure your application can scale to meet demand? You got it: Durable Objects.
No matter what the SaaS is, you can likely use Durable Objects to represent the key resources that your users can create. Whether it’s an AI Agent, a hotel reservation system like Booking.com, or data storage like Dropbox - the underlying things that users create can be individual Durable Objects.
We’ll touch on it more later, but in the example of the hotel reservation system, you’d want to avoid double booking when two users submit a reservation at the same time. Durable Objects can prevent this being an issue, as each one is single-threaded and queues up requests without interweaving them. With the state being held within the Durable Object, the first request would secure the booking, and the second request would return an error to the user when the availability was checked.
Let’s cover one more, and this could be a very typical application that has no multi-tenancy, no real-time - just your regular CRUD application.
Enjoying the article and want to get a head start developing with Cloudflare? I’ve published a book, Serverless Apps on Cloudflare, that introduces you to all of the platform’s key offerings by guiding you through building a series of applications on Cloudflare.
Buy now: eBook | Paperback
Getting Started with Durable Objects
Cover Prerequisites (Cloudflare Account, Wrangler, etc.) (now free!)
To help you better understand Durable Objects, let’s take a look at how to create and deploy one. Because I mentioned that they are like mini servers before, you might be imagining we’ll need to provision something, but we absolutely do not.
Before you can create and deploy Durable Objects, you’ll need a free Cloudflare account. You’ll also need to install Wrangler, Cloudflare’s CLI. Once you’ve got both, let’s get creating!
Creating a Durable Object
The best way to start pretty much any Cloudflare project, is by using npm create
, and Durable Objects are no exception. Run the following to create the scaffolding required to use Durable Objects:
npm create cloudflare@latest -- durable-object-starter
You can choose whatever name you like for your project. When you run the command, you’ll effectively get a menu of options to choose from. Select the following:
Hello World example
Worker + Durable Objects
TypeScript (you can use JS if you prefer)
Yes to using Git
No to deploying, we’ll do that later
If you want to try out other Cloudflare products, feel free to poke around the other menu options and see what’s available.
All of the code that’s related to Durable Objects will be in src/index.ts
. The Durable Object definition is pure code, and looks just like any other JavaScript or TypeScript class.
export class MyDurableObject extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}
async sayHello(name: string): Promise<string> {
return `Hello, ${name}!`;
}
}
This is what will allow you to effortlessly spin up as many Durable Objects as you need, because you create Durable Objects on-the-fly with code. That makes them scale to the moon, and I also really like that everything is code and you interact with a Durable Object as if it were a regular JavaScript class. We’ll get into how to create instances and how the communication works later on.
For now, looking at the Durable Object class definition, there’s a few things to callout. First, there’s the constructor, which has two parameters:
ctx (short for context) is where you’ll find some of the bells and whistles that come built-in to a Durable Object. For example, there are methods to interact with WebSockets, and methods to interact with storage (both key-value and SQLite are supported!)
env is a common parameter across Durable Objects and Workers, and you can think of this as like the environment your Durable Object has access to. There are things you’d expect from other programming languages, like environment variables and secrets, but also Cloudflare-specific magic such as bindings.
What are bindings, you might ask? This could honestly be an entire post on its own, but think of them as a way to link your compute (a Worker, a Durable Object) to a resource such as a cache, database, queue or, as we’ll see shortly, a Durable Object.
One of the really cool things about bindings is, they are injected automatically at runtime, depending on the environment your compute is operating in. You can bind different resources based on development, staging and production, for example. Then at runtime, env will contain objects for you to use to interact with these resources (e.g. reading from a KV cache by calling env.KV_CACHE.get(‘your-key’)
)
To see this in action, let’s look at the code that’s been generated that creates Durable Objects on-the-fly using a Worker:
export default {
async fetch(request, env, ctx): Promise<Response> {
const id: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName("foo");
const stub = env.MY_DURABLE_OBJECT.get(id);
const greeting = await stub.sayHello("world");
return new Response(greeting);
},
};
If you’re new to Cloudflare, and haven’t seen a Worker before, this is the basic structure of a Cloudflare Worker. The most common way to interact with them is via HTTP, as Workers, by default, are public and accessible by hitting a URL. When a request comes in, the fetch function is called, and you can then handle the HTTP request with standard JavaScript code.
In the example code above, a binding to the Durable Object we looked at earlier is used to create an instance of that Durable Object, call its sayHello
method and return a response.
A key thing to note is that while the call to sayHello
might look like a regular in-memory method call, you’re actually calling methods on a stub, that makes RPC calls to your Durable Object that is likely sitting in another data centre alongside its storage.
You can add as many methods as you like to your Durable Object, in the same way you would any other class. There are other built-in capabilities, such as alarms and WebSockets, that we’ll cover later on.
If you want to see everything working locally, simply run npm run dev
in your terminal, and open the URL shown. Another big feature of Cloudflare’s developer platform is that you can (nearly always) run everything locally, with Cloudflare simulating bindings locally too.
This is true for Durable Objects as well. Keep in mind there will be no latency locally, so it’s not a true representation of the performance you’ll see once deployed. For that, you can run npm run deploy
and your Worker will be deployed to Cloudflare’s global network, along with your Durable Object.
Once again, open the URL output in the terminal and your Worker will serve the request by communicating with your Durable Object.
Now that you understand how to construct and deploy a Durable Object, let’s take a closer look at some core concepts and the underlying architecture of Durable Objects.
Core Concepts of Durable Objects
State Management and Consistency
As we’ve covered already, one of the key aspects of a Durable Object is the fact it comes with built-in durable storage. This storage is colocated with the Durable Object itself, so the latency to its storage is basically non-existent.
When Durable Objects were first released, they only came with a very simple key-value store. This was useful, but it made storing complex, relational data quite cumbersome.
In late 2024, a second storage option was released, allowing you to harness the power of SQLite within your Durable Object. As you can imagine, this gives you a lot more freedom and flexibility in how you structure and store your data.
Additionally, because the storage is colocated with the Durable Object, queries are effectively zero-latency. This is because SQLite is being run not only in the same data centre, but in the exact same thread as the compute for your Durable Object. Database queries are also synchronous, because they are executed so rapidly there’s no need to make them async.
If you’ve ever worked with SQL before, you’ll likely have heard of the N+1 problem, but with SQLite-backed Durable Objects, you can genuinely write these types of queries and not incur any noticeable performance hit.
In the case of writes, they too are synchronous, but they are still durable and written to disk before a response is returned from your Durable Object. This is where some clever engineering comes in on Cloudflare’s part, as the code doesn’t wait for the query to be written to disk before continuing, but you’re still protected from inconsistencies thanks to what Cloudflare call Output Gates.
Rather than regurgitate Kenton’s blog post, I’ll just quote him verbatim:
In DOs, when the application issues a write, it continues executing without waiting for confirmation. However, when the DO then responds to the client, the response is blocked by the "Output Gate". This system holds the response until all storage writes relevant to the response have been confirmed, then sends the response on its way. In the rare case that the write fails, the response will be replaced with an error and the Durable Object itself will restart. So, even though the application constructed a "success" response, nobody can ever see that this happened, and thus nobody can be misled into believing that the data was stored.
You can see this in action in the following diagram:

There are other benefits to this approach, such as lower latency when handling requests, as the Durable Object can work on building the response while the storage layer confirms the write, actually speeding up overall request handling time.
Isolation and Concurrency Model
As previously mentioned, Durable Objects are a special kind of Worker, but they still use the same runtime and how that runtime is operated is the same.
The runtime is called V8, and it’s the same that runs when you’re using Google Chrome. In terms of isolation, you can think of it like tabs in your browser, where one tab cannot access data in another tab.
The same is true for Workers and Durable objects, that run in something called an isolate. To quote Cloudflare’s documentation:
V8 provides lightweight contexts that provide your code with variables it can access and a safe environment to be executed within. You could even consider an isolate a sandbox for your function to run in.
It’s actually an incredibly efficient way to run software. There are no containers, and within a single process, you can run hundreds or thousands of isolates, each completely separate and unable to access one another’s data.
As an extra benefit, in the same way your tabs open instantly, isolates are near-instant in terms of start time too. That means there’s rarely cold starts on Cloudflare’s platform, as your code can be loaded ready to be executed during the TLS handshake.
Unlike Lambda, isolates are typically rapidly evicted from memory after they have finished executing code. That makes in-memory caching largely useless, but if your Durable Object is busy and handling lots of concurrent requests, it will stay around for a few seconds, in which case you can see benefit from in-memory caching.
But to really ram the point home, while the storage layer of a Durable Object is persisted between evictions, in-memory data is not. This is important to keep in mind, as looking at the code, you might be fooled into thinking the constructor worked in the same way as a standard class, where you’re able to initialize and cache data on creation.
Location and Global Distribution
As with the majority of Cloudflare’s developer platform, Durable Objects are automatically available globally. They are not available in as many locations as Workers are (330+ cities!) but they are definitely globally-distributed across the continents of the world.
Their initial location is determined when you first create a Durable Object, and it will be created close to where your Worker is calling from. It might be in the same data centre, but if not, it will be in close proximity.
Due to the nature of Durable Objects, and the fact they are created dynamically, the assumption here is that whoever is calling your application likely wants to keep accessing the data you’ll store in a Durable Object.
For example, consider Google Docs, if you create a document, you’re likely to be the primary person editing and working on that document. Therefore, to improve the responsiveness of the application, it makes sense the Durable Object lives close to that end user.
Naturally, there might be occasions where you want to set the location of a Durable Object yourself. Perhaps the person using a SaaS application is in Australia, but the majority of their customers are in the US. In that case, you might want to offer them the ability to create any documents in the US, to reduce latency to their customers.
This is possible at creation by specifying a location hint when you retrieve a Durable Object:
let durableObjectStub = DO_NAMESPACE.get(id, { locationHint: "enam" });
You can see the full list of available locations here.
Another quirk of Durable Objects is that once their location is set, it typically won’t move from that data centre, and you can’t request it be moved either. If you wanted to move the location of a Durable Object, you’d need to perform your own migration, where you create new Durable Objects in a different location, and then migrate any storage.
This doesn’t mean that if that data centre goes down that your Durable Object is inaccessible though, as Cloudflare can opt to move your Durable Object around if needed. There are backups of your Durable Object held in other locations to facilitate this. This is all managed by the platform though, and again, you have no control over when and if this happens.
Durable Objects API
With a solid understanding of what Durable Objects are and how they are architected, let’s take a closer look at the APIs available - there’s everything from RPC, to WebSockets, to AI agents and more to cover.
Synchronous Calls with Fetch & RPC
Let’s start out with the simple stuff: you can synchronous call your Durable Object via fetch or RPC. For a while, there was only fetch, so if you wanted to provide more than one piece of functionality from a Durable Object, you basically had to implement path-based routing in the same way you would a HTTP-based API.
This had other drawbacks, as you had to serialize/deserialize JSON for the payloads on either side of the call. This isn’t the end of the world, but it does involve a lot more code versus RPC. Here’s what a call might look like if you wanted to use a Durable Object for authentication from a Worker:
// Get the Durable Object
const id = env.MY_DURABLE_OBJECT.idFromName('foo');
let stub = env.MY_DURABLE_OBJECT.get(id);
// Construct a JSON request to the auth service.
let authRequest = {
cookie: req.headers.get("Cookie")
};
// Send it to the Durable Object
let resp = await stub.fetch(
"https://auth/check-cookie", {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify(authRequest)
});
if (!resp.ok) {
return new Response("Internal Server Error", {status: 500});
}
// Parse the JSON result.
let authResult = await resp.json();
// Use the result.
if (!authResult.authenticated) {
return new Response("Not authenticated", {status: 401});
}
let username = authResult.username;
Considering you likely want to make calls to Durable Objects and other Workers throughout your application, the amount of code needed to make a call rapidly adds up.
Luckily, in 2024, Cloudflare added support for calling Durable Objects using RPC (alongside Worker-to-Worker calls as well). For anyone not familiar with RPC, it’s effectively an alternative to using a protocol such as HTTP, where you make calls on a stub object (that looks like regular method calls), but behind the scenes, RPC handles all the serializing/deserializing for you as well as transmitting the data.
This ends up removing a lot of the boilerplate code above:
const id = env.MY_DURABLE_OBJECT.idFromName('foo');
let stub = env.MY_DURABLE_OBJECT.get(id);
// Call the Durable Object
let resp = await stub.authenticate(req.headers.get("Cookie"));
// Use the result.
if (!resp.authenticated) {
return new Response("Not authenticated", {status: 401});
}
let username = resp.username;
I think we can all agree this is a lot simpler and cleaner. Additionally, let’s say we wanted this Durable Object to handle authentication and authorisation, we could easily add an authorized method to the Durable Object and call that too. If we were limited to fetch, we’d have to introduce some path-based routing to handle this.
It’s a lot easier to deal with regular objects too, as it means, if you’re using TypeScript, you can have type definitions for the inputs and outputs and simply pass regular data types back and forth. As long as what you pass is able to be cloned, you’re good to go.
You can even pass callbacks in the form of functions, and if that function is called, an RPC call will be made automatically back to the caller.
Schedule Execution with Alarms
If you need a Durable Object to wake itself up and execute some code on a schedule, then you’re in luck, as they come with alarms built-in. Much like the alarm you set to wake up in the morning, you simply set an alarm programatically, and the alarm method of your Durable Object will be called at that time.
To bring this concept to life, here’s what it looks like in code:
export class AlarmExample extends DurableObject {
constructor(ctx, env) {
this.ctx = ctx;
this.storage = ctx.storage;
}
// Called via RPC
async schedule() {
// If there is no alarm currently set, set one in 10 seconds
let currentAlarm = await this.storage.getAlarm();
if (currentAlarm == null) {
this.storage.setAlarm(Date.now() + (10 * 60));
}
}
// Define your logic to execute here
async alarm() {
}
}
As you can see, it uses the underlying storage of the Durable Object to store the alarm. It’s only possible to set one alarm per Durable Object, but you can write your own logic to manage multiple alarms if you need to.
Alarms have retries baked in, so if your alarm handler throws an error, it will be automatically retried up to 6 times with exponential backoff.
If you’re wondering how you might use an alarm for a real-world use case, let’s say you’re building a website that provides stock market data. You could create a Durable Object per company, and schedule an alarm that periodically wakes up (let’s say every 10 seconds) to retrieve the latest data, storing it within the Durable Object itself.
This saves you having a cron-like task that loops through hundreds of companies, and has some nice benefits like isolating failures to just one company, if one particular company’s data isn’t available.
Speaking of storage, let’s take a look at the two flavours of storage available to you when using Durable Objects.
Store Simple Data with Key-Value Storage
When Durable Objects were released a few years ago, they did so with just one option for storage: a key-value store. Just for clarity, it shouldn’t be confused with Workers KV - as they are completely different in how they work.
In the case of a Durable Object, the storage is maintained by the Durable Object itself. Unlike Workers KV, it’s not globally replicated in a transactional sense, because it doesn’t need to be due to the fact the DO lives in one place at one time.
The interface itself will be familiar to anyone who has used a key-value store. Here’s a simple example using a counter to illustrate the API:
export class Counter extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
}
async increment() {
let value = (await this.ctx.storage.get("value")) || 0;
value += 1;
await this.ctx.storage.put("value", value);
return value;
}
}
The key lines here are this.ctx.storage.get("value")
and this.ctx.storage.put
, which do exactly as they sound: get data and store data.
Alongside get and put, there’s delete to remove data, and list to retrieve all the key-value pairs.
It works well for simple use cases, but can become unwieldy if you need to store more complex data, or even just a large amount that isn’t suitable for key-value format. This is largely due to the fact the maximum value you can store is 128 KiB per key. Additionally, you can only store 50GB across your entire account, although this can be raised by request.
If you need more complex storage, then you’re in luck - let’s take a look at the second storage option.
Store Relational Data with SQLite
In September 2024, during Birthday Week, Cloudflare gave us quite the present in the form of SQLite-backed Durable Objects. This announcement added a second storage option alongside key-value, providing the ability to run queries against a SQLite database.
The incredibly cool part of this is the storage is effectively zero-latency, as the SQLite database exists in the same thread as your Durable Object. This means you can create schemas, retrieve data and insert data pretty much instantaneously.
When compared to the key-value storage, the limits are significantly higher for SQLite-backed Durable Objects. You can store up to 10GB per Durable Object, with no limit on the amount of data you can store across your account (note: the free plan limits you to 5GB).
This sort of relational data format unlocks the ability to treat your key domain entities as living objects, at least in my opinion. There’s naturally a conversation to be had around vendor lock-in, but imagine being able to represent your key domain entities and have their lifecycle managed by a Durable Object.
Let’s say you’re a chat company, something akin to Zendesk. The key entity in your domain is likely a chat, and with Durable Objects each chat would be its own Durable Object instance.
All the history would be stored in the Durable Object, and as we’ll dive into next, the client can connect to a chat via WebSockets directly into the Durable Object. Additionally, let’s say you want to send a feedback request email 24 hours after the last message is sent - simply define an alarm, and the DO can send that email itself.
It takes some getting used to, but there’s something really unique about seeing your application behave as if it’s a living ecosystem. It’s a little like the first time you go to see the a theatre show or the ballet, you’re in awe at the coordination that just seems to magically happen before your eyes.
So what does a SQLite-backed Durable Object look like? Let’s take a look:
export class FlightSeating extends DurableObject {
sql = this.ctx.storage.sql;
// Call when the flight is first created to set up the seat map.
initializeFlight(seatList) {
this.sql.exec(`
CREATE TABLE seats (
seatId TEXT PRIMARY KEY, -- e.g. "3B"
occupant TEXT -- null if available
)
`);
for (let seat of seatList) {
this.sql.exec(`INSERT INTO seats VALUES (?, null)`, seat);
}
}
// Get a list of available seats.
getAvailable() {
let results = [];
// Query returns a cursor.
let cursor = this.sql.exec(`SELECT seatId FROM seats WHERE occupant IS NULL`);
// Cursors are iterable.
for (let row of cursor) {
// Each row is an object with a property for each column.
results.push(row.seatId);
}
return results;
}
As you can see, you make use of the storage object on the context object within the Durable Object, this time using sql. It uses a cursor for access, which is straightforward to use, alongside regular insert and create statements that are standard within SQLite. The full API can be seen here.
Additionally, SQLite-backed Durable Objects come with point-in-time recovery. You can restore your SQLite database to any point in time in the last 30 days, using a straightforward API.
Now, you might be thinking, what if I need to add or update the schemas in a Durable Object? Well, thanks to fact that the queries are lightening fast, I actually tend to do any migrations in the constructor when needed on-the-fly.
The constructor will be called each time the Durable Object is woken up. So let’s say you get a Durable Object with an ID of test, the constructor will be called, and then any subsequent requests won’t trigger the constructor.
As a Durable Object will hang around in memory for a little while, if you make lots of requests in quick succession, the constructor will only be called once. As a nice little caching strategy, this means you can store instance variables as a form of very temporary cache, where they will only be present until the Durable Object is evicted from memory.
I’d like to see some tooling for this baked into the Durable Object API, in the same way D1 databases track which migrations have been applied, it would be helpful if Durable Objects could do this, and then (optionally) auto-apply any migrations needed.
If you want to reduce the amount of code you have lying around, you could have a script that loops over all your Durable Objects and applies any schema changes, as an alternative strategy - but it will take a while if you have a lot of them, and naturally use up quite a lot of requests.
It’s worth noting that while key-value-based Durable Objects will be around for a while, the recommendation is to use SQLite-backed Durable objects, which also still have access to a key-value store.
We touched on WebSocket support in this section, so let’s dive a little deeper into that.
Built-in WebSocket Support
I’ve mentioned multiplayer a few times, and in the last section mentioned a chat application that could use WebSockets to enable real-time communication. Durable Objects are perfect for this use case because they come with baked-in WebSocket support.
Before I used Durable Objects, I’d never used WebSockets, and from what I’d heard, they could be a bit of a pain to deal with. Now, whether that’s true or not, I can’t say as I’ve never had a reason to use anything beside a Durable Object, it’s that easy.
You can genuinely setup a WebSocket-based application in a few lines of code in the server-side, and just use typical connection code on the client side. The interface is really clean too:
export class WebSocketHibernationServer extends DurableObject {
async fetch(request) {
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
this.ctx.acceptWebSocket(server);
return new Response(null, {
status: 101,
webSocket: client,
});
}
async webSocketMessage(ws, message) {
ws.send(
`[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`,
);
}
async webSocketClose(ws, code, reason, wasClean) {
ws.close(code, "Durable Object is closing WebSocket");
}
}
To quickly explain what’s happening here, a Worker would invoke the Durable Object using the fetch function when it wants to establish a WebSocket connection. The response contains the client-side WebSocket, as well as a HTTP 101 header which indicates the client wants to switch the protocol from HTTP.
The Durable Object simply calls acceptWebSocket
, and that’s all there is to it. When a message is received from the client, the webSocketMessage
method will be called, and you can handle the incoming message as required.
You can connect a large number of clients to a single Durable Object instance, with no published limit from Cloudflare. The number of effective WebSockets you can connect to a single instance is going to depend how busy each WebSocket is, and what happens when each message is received.
Keep in mind that the Durable Object can “only” handle 1,000 requests per second at peak, so you may need to shard your Durable Objects to ensure they remain performant.
Going back to the chat example, let’s say you have 5 people connected to a single chat. You’ll want to relay any message sent to all clients, which is easy to do by calling this.ctx.getWebSockets
, looping over each one and sending a message.
You might be thinking, wait, what if I have all the clients connected to my Durable Object, isn’t that going to cost a fortune as the Durable Object will be active continuously?
Well, want to know something else cool? Durable Objects, will hibernate between handling WebSocket messages. Let’s say you have a chat, and no one has sent any messages for 30 minutes, but they have the tab open in their browser. The Durable Object has been hibernating the entire time, and only wakes up when a message is sent. Therefore, you only pay for the wall time (duration) your Durable Object uses to handle incoming WebSocket messages.
This is a pretty killer feature, and honestly the interface is so simple, I think it’s really well done, and I thoroughly enjoying building WebSocket-based applications these days.
Agents SDK
It wouldn’t be a post in 2025 without some AI, would it?
It’s honestly worth checking out agents.cloudflare.com for an overview of the capabilities Cloudflare provides for building agents. Not only is the website beautiful, it does an excellent job of explaining some AI concepts alongside how Cloudflare enables them.
In effect, the Agents SDK from Cloudflare wraps Durable Objects to provide key capabilities needed to create AI agents. When you think back to what I said about Durable Objects being excellent ways to create living objects in your ecosystem, this is exactly what the Agents SDK is doing for AI agents.
It comes with baked-in React hooks as well, so you can easily connect clients to your AI agents. If you’ve been living under a rock lately, and somehow missed the hype, MCP is all the rage right now.
I’ll be honest, I’m not entirely sold on MCP living up to the hype just yet, it feels a little like a bubble currently. I can totally see how it could be huge in the future, but for now, it feels mainly like companies are riding the hype wave and tangible use cases for MCP are few-and-far between.
Ignoring that little tangent, the Agents SDK comes with MCP support - both in terms of connecting your AI agents to remote MCP servers, as well as hosting your own MCP servers.
Honestly, the SDK comes with so much, so here’s a quick roundup:
Server-side and client-side agent classes
WebSockets
State synchronization
Scheduling
SQLite support (using SQLite-backed Durable Objects)
MCP support
Server-Sent Events
Browse the web using Puppeteer, via Browser Rendering
And to be honest, there’s more but I highly recommend checking out the website for more information plus the docs.
How can I observe what my Durable Object is doing?
One of the key aspects to using any technology is how easy is it to understand what it’s doing, and investigating issues when they arise?
I will admit, one area that is in need of some love is the observability for Durable Objects. It has steadily improved, especially with the introduction of Workers Logs, which allows you to query and filter logs across your Workers and Durable Objects.
With the acquisition of Baselime, the observability aspect of the Cloudflare platform has steadily improved year-on-year, and I know there are more things in the works.
The data side is more tricky, however. How do you view what each Durable Object contains within its storage? Well, while it’s not particularly easy right now, there is growing tooling for it.
Outerbase was acquired by Cloudflare this year, and they already provide Outerbase Studio that can be configured to access Durable Objects using SQL. With the acquisition, I’m hoping this functionality finds its way into the Cloudflare ecosystem, hopefully allowing you to query data held across your Durable Objects.
How much does running a Durable Object cost?
At this point, hopefully you’re excited to try out Durable Objects. Before you do though, you probably want to know how much it’s going to cost you, right?
Well, I’ve got you covered. The good news is Durable Objects are recently available on the free tier, meaning you get 100,000 requests per day for free and 13,000 GB-s per day too.
If you’re confused by the GB-s, you’re not alone. The GB-s metric means 1 GB used continuously for 1 second. It's calculated by multiplying the amount of storage or memory used (in GB) by the duration it's utilized (in seconds).
Keep in mind you’re charged for the maximum memory available, not the memory you actually allocate. That means on the free plan you can run a little over one Durable Object for the entire day, and not get charged. Assuming my calculations are correct, you get just under 29 hours of Durable Object duration per day for free.
Let’s say you really love Durable Objects, you build all your applications with it, and suddenly you’re processing a ton through them. Here’s what it’s going to cost you:
$0.15 per million requests
$12.50 per million GB-s
Helpfully, there’s a reduced cost for WebSocket messages, which aren’t billed 1:1 in terms of requests:
There is no charge for outgoing WebSocket messages, nor for incoming WebSocket protocol pings. For compute requests billing-only, a 20:1 ratio is applied to incoming WebSocket messages to factor in smaller messages for real-time communication. For example, 100 WebSocket incoming messages would be charged as 5 requests for billing purposes.
There are separate charges for the storage used too. Starting out with the key-value option, which doesn’t have a free plan, but does come included in the $5/month Workers Paid plan:
1 million read requests/month (+$0.20/million after)
1 million write requests/month (+$1.00/million after)
1 million delete requests/month (+$1.00/million after)
1 GB of stored data (+ $0.20/ GB-month after)
Moving onto the SQLite-backed Durable Objects, which do have a free tier:
5 million rows read/day
100,000 rows write/day
5GB total storage
The limits then get significantly bumped on the Workers Paid plan, with additional charges beyond that:
25 billion rows read/month (+$0.001 / million rows after)
50 million rows written/month (+$1.00 / million rows after)
5GB total storage (+$0.20/GB-month)
That’s a lot of billing information I know, but I think it’s good to consider and dive into a little. We’ll round out this post by looking at real-world examples and some use cases featuring Durable Objects.
Real-world Examples and Case Studies
I’m going to share a few websites that I’ve come across that use Durable Objects. I’d honestly love to see more companies who are using them share their stories, both the good and the bad, as the blog articles sharing insights are few and far between.
While Durable Objects are still spreading in terms of usage, I know for sure they are used far more widely than it visible online. Having said that, here’s a few:
tldraw is a white boarding experience, offering an infinite canvas and uses WebSockets to synchronize all client activity.
Gitlip is a collaborative coding platform, utilizing Durable Objects to house the underlying code for each repository. There’s an excellent deep dive on how they built it here.
Waiting Room is a product from Cloudflare, and I’m highlighting this because a ton of Cloudflare products are effectively built on Durable Objects. This blog article provides good insights into how and why they are using them for this common use case
Enjoyed the article and want to get a head start developing with Cloudflare? I’ve published a book, Serverless Apps on Cloudflare, that introduces you to all of the platform’s key offerings by guiding you through building a series of applications on Cloudflare.
Buy now: eBook | Paperback
Wrapping Up
That brings us to the end of my ultimate guide to Durable Objects, if you made it this far, thank you for reading and I hope you found the content useful.
I truly think Durable Objects allow you to build applications in completely different and novel ways, and while they take some getting used to, they are incredibly powerful and I really hope we see more and more examples of them used in the wild.
If you have any questions or think I missed something, let me know and I’ll do my best to respond and update the article, and likewise if you think something is incorrect!