iTranslated by AI
Understanding Durable Objects in Cloudflare Workers
Unless otherwise specified, all quotes are from Workers Durable Objects Beta: A New Approach to Stateful Serverless translated via DeepL.
As prerequisite knowledge, I will not explain Cloudflare Workers itself here. Please refer to this: Cloudflare Workers: Extending Frontend / Node.js to the CDN Edge Side (Japanese)
In summary, Cloudflare Workers is a serverless environment where low-latency, low-spec CPU Node.js workers run at high speed on the CDN Edge.
What is Durable Objects?
Note: It is still in closed beta.
Originally, I intended to write this article after trying it out firsthand, but even after applying for the closed beta, access hasn't been granted. Therefore, I'm writing this based on the information currently available.
Durable Objects is a feature for persisting memory state within Cloudflare Workers.
Unlike ordinary JavaScript objects, a Durable Object can have persistent state stored on disk. Each object’s durable state is private to itself, so as well as having fast access to storage, objects can safely maintain a consistent copy of state in memory and even perform operations on it with zero latency. In-memory objects are shut down when idle and later recreated on-demand.
What can you do with Durable Objects?
First, let's look at the atomic counter example provided as sample code.
export class Counter {
constructor(controller, env) {
// `controller.storage` is an interface to access the object's
// on-disk durable storage.
this.storage = controller.storage
}
// Private helper method called from fetch(), below.
async initialize() {
let stored = await this.storage.get("value");
this.value = stored || 0;
}
// Handle HTTP requests from clients.
//
// The system calls this method when an HTTP request is sent to
// the object. Note that these requests strictly come from other
// parts of your Worker, not from the public internet.
async fetch(request) {
// Make sure we're fully initialized from storage.
if (!this.initializePromise) {
this.initializePromise = this.initialize();
}
await this.initializePromise;
// Apply requested action.
let url = new URL(request.url);
switch (url.pathname) {
case "/increment":
++this.value;
await this.storage.put("value", this.value);
break;
case "/decrement":
--this.value;
await this.storage.put("value", this.value);
break;
case "/":
// Just serve the current value. No storage calls needed!
break;
default:
return new Response("Not found", {status: 404});
}
// Return current value.
return new Response(this.value);
}
}
The storage looks like a persistence layer. fetch is the fetch handler, where /increment and /decrement are implemented.
While not shown in this example, it also supports the WebSocket protocol. Here is a part of the chat demo code:
export class ChatRoom {
// ... omitted
async fetch(request) {
return await handleErrors(request, async () => {
let url = new URL(request.url);
switch (url.pathname) {
case "/websocket": {
if (request.headers.get("Upgrade") != "websocket") {
return new Response("expected websocket", {status: 400});
}
let ip = request.headers.get("CF-Connecting-IP");
let [a, b] = new WebSocketPair();
// We're going to take pair[1] as our end, and return pair[0] to the client.
await this.handleSession(b, ip);
// Now we return the other end of the pair to the client.
return new Response(null, { status: 101, webSocket: a });
}
default:
return new Response("Not found", {status: 404});
}
});
}
}
And the client code that calls it:
let ws = new WebSocket("wss://" + hostname + "/api/room/" + roomname + "/websocket");
// ...
ws.addEventListener("message", event => {
let data = JSON.parse(event.data);
if (data.error) {
addChatMessage(null, "* Error: " + data.error);
} else if (data.joined) {
let p = document.createElement("p");
p.innerText = data.joined;
roster.appendChild(p);
} else if (data.quit) {
for (let child of roster.childNodes) {
if (child.innerText == data.quit) {
roster.removeChild(child);
break;
}
}
} else if (data.ready) {
// ...
This is implemented using the raw WebSocket protocol without relying on libraries.
Applications mentioned for this include chat, session management for multiplayer games, social feeds, and shopping carts.
Difference from Workers KV
Two years ago we introduced Workers KV, a global key-value data store. KV is a fairly minimalist global data store, which is good for some purposes, but not for everyone. KV is eventually consistent, meaning that a write made in one location may not be immediately visible in others. Furthermore, KV implements “last-write-wins” semantics, meaning that if a key is modified from multiple locations around the world at the same time, those writes are prone to overwriting each other. KV was designed this way to support low-latency reads for data that does not change often. These design decisions, however, make KV a poor fit for state that changes frequently, or where changes need to be seen immediately around the world.
In contrast, Durable Objects are not primarily a storage product. Durable Objects are on the opposite end of the spectrum from KV when it comes to the storage they provide. Durable Objects are great for workloads that require transactional guarantees and immediate consistency. Transactions, however, must inherently be coordinated in a single location, and clients from the opposite side of the world from that location will experience moderate latency due to the inherent limits of the speed of light. Durable Objects deal with this by automatically moving to live near where they are used.
In short, while Workers KV remains the best way to serve static content, configurations, and other rarely-changing data around the world, Durable Objects are better for managing dynamic state and coordination.
In other words, Workers KV is eventually consistent, while Durable Objects are strongly consistent. I wondered what happens when users are geographically distant, but there is the following description:
When using Durable Objects, Cloudflare automatically determines the Cloudflare data center in which each object lives, and can transparently migrate objects between locations as needed.
So, if access is from the same region, it should be fast, but if objects are shared among people who are geographically distant, reading and writing might become slower.
Future Prospects: The Future of Edge Distributed Databases
We see Durable Objects as a low-level primitive for building distributed systems. Some of the applications above can use objects directly to implement coordination layers or even as the sole storage layer.
However, as they stand today, Durable Objects are not a complete database solution. Each object only sees its own data. To perform queries or transactions across multiple objects, an application would have to do some extra work.
That said, all large distributed databases (relational, document, graph, etc.) are built of low-level pieces. At a low level, a large distributed database is composed of "chunks" or "shards" which each store some part of the overall data. The job of the distributed database is to coordinate between chunks.
We see a future of edge databases where each "chunk" is stored as a Durable Object. In this way, it will be possible to build databases that run entirely on the edge, fully distributed, with no region or home location. We don’t have to build these databases; anyone can build them on top of Durable Objects. Durable Objects are only the first step in our edge storage journey.
Looking at this, I was reminded of Akka.
Introduction to Akka for Concurrency Beginners (Japanese)
Of course, it's not about fault tolerance like Akka, and it's specifically about Durable Objects, but if Durable Objects can communicate with each other in the future, it might be possible to think of them as Actors.
In the current Web Application paradigm, the mainstream is to persist all states once into a database. However, with Durable Objects, the option to have short-to-medium-term processing handled at high speed through persistence inside these actors will increase.
Past Similar Cases
Actually, Azure has a similar feature, but the significance of doing this on Cloudflare Workers is that efficiency through relocation at the CDN level can be pursued, making it likely that high-speed workloads can be realized.
Durable Functions Overview - Azure | Microsoft Docs
Furthermore, until now, it was somewhat possible to do something similar with WebSocket sticky sessions, but handling sticky sessions was often troublesome or impossible depending on k8s or the load balancers of each cloud provider, which was quite a headache.
So, is it usable?
It's still an unknown quantity, but if you're currently implementing chat using something like Pusher, it should be possible to replace it with Durable Objects. WebSockets have been a somewhat neglected protocol, so I'm happy to see realistic use cases finally emerging.
When considering production-level deployment, since the current API itself is only primitive, I think it will be necessary to build a dedicated framework for it.
Discussion