Collaborative features have a habit of pulling a surprising amount of infrastructure behind them.
Say we are building a React app where several people can open the same board. Someone moves a card, edits a field or adds a comment, and everyone else should see it almost immediately.
A fairly normal architecture might end up looking like this:
There is nothing wrong with that setup. But there is an interesting alternative with Cloudflare Durable Objects.
Give every room one object.
That object owns the connections for the room, receives its events and tells everyone else what changed.
The interesting part is not that Durable Objects support WebSockets. Plenty of things support WebSockets.
It is what we no longer need around them.
WebSockets do not solve coordination
Imagine Alice and Bob connect to server A while Charlie connects to server B.
Alice changes the title.
Server A can tell Bob immediately because it owns Bob's socket. Charlie is harder. Server A has no access to a WebSocket sitting inside another process.
So we introduce something shared:
Redis Pub/Sub works well for this. Kafka, NATS and other systems can solve versions of the same problem too.
But our collaborative feature now needs another service simply so one server can tell another server what happened.
With Durable Objects, we can move the boundary.
One room, one object
Every Durable Object has a unique identity.
Instead of connecting users to any WebSocket server, we can route everyone viewing room_123 to the Durable Object for room_123.
Conceptually:
const room = env.ROOMS.getByName(roomId)
return room.fetch(request)Now Alice, Bob and Charlie all talk to the same coordinator.
When Alice sends:
{
"type": "card:moved",
"cardId": "42",
"columnId": "done"
}the object can validate the message, update the room and broadcast the result to its connected clients.
There is no Redis channel to publish to. There is no second WebSocket server that needs to hear about it.
The object already owns the room.
What actually disappeared?
This is where Durable Objects get interesting.
With a normal pool of WebSocket servers, we need answers to questions like:
Which server owns each connection?
How does server A reach clients connected to server B?
Which Redis channels should each server subscribe to?
What happens when a server disappears?
How do we clean up subscriptions?
Do we need sticky sessions?
How do we stop several servers from making conflicting changes to the same room?
When every room has one coordinator, several of those questions disappear.
We have taken something spread across several machines and given it a clear owner.
That makes the code easier to reason about.
React barely needs to know
The React side is intentionally boring.
It opens a socket:
const socket = new WebSocket(`/rooms/${roomId}`)
socket.addEventListener("message", event => {
const message = JSON.parse(event.data)
if (message.type === "card:moved") {
updateCard(message.cardId, message.columnId)
}
})And sends changes back:
socket.send(JSON.stringify({
type: "card:moved",
cardId,
columnId
}))I would add event IDs, versions and optimistic updates in a real application, but those are another problem.
Collaborative editing can get much harder once two people edit the same thing at the same time. You eventually need to think about server ordering, conflicting updates and, for some products, approaches such as CRDTs.
Durable Objects do not make that problem disappear. They just give you a useful place to solve it.
The room can own data too
Durable Objects also have persistent storage tied to the object.
That means some state naturally belongs beside the coordinator:
- current version
- participants
- presence
- recent operations
- connected WebSockets
I would not move an application's entire database into Durable Objects because of this.
Users, billing, reporting and anything that needs broad queries may still fit Postgres much better.
The useful rule is simpler: data that only makes sense inside a room can often live with the room.
Hibernation changes the cost model
There is one detail I would not skip when building this for real: WebSocket hibernation.
A normal WebSocket can keep a Durable Object in memory. Cloudflare bills Durable Object compute using wall clock duration while the object remains active or cannot hibernate.
That matters for collaborative apps because sockets can stay connected for hours while doing almost nothing.
Cloudflare provides a separate WebSocket Hibernation API. The client remains connected to Cloudflare, but the Durable Object can leave memory when nothing is happening. When another message arrives, Cloudflare wakes it again.
So you stop paying duration charges just because somebody left a browser tab open.
The difference can be significant. Cloudflare's current pricing examples put one regular WebSocket workload at about $143 per month and one hibernating workload at about $21 per month. Those examples use different traffic patterns, so they are not a direct price comparison, but they show why Cloudflare recommends hibernation for this kind of application.
There is a consequence though.
When the object wakes up, its old in memory JavaScript state is gone. Anything important needs to live in storage or be recoverable from the WebSocket connection metadata.
That changes how I would design the room from the start.
There is still a network
Durable Objects also introduce a latency tradeoff that is easy to miss.
An object runs in one location. By default, Cloudflare creates it near the request that first caused it to exist, and it does not currently move that object later.
So if Alice creates a room from Birmingham and Charlie later joins from Sydney, Charlie's messages still need to reach the location hosting that room.
Cloudflare's network handles the routing, but physics has not gone anywhere.
For a board, chat or planning tool that may be completely acceptable. For something where every millisecond matters, location becomes part of the design. Cloudflare also supports location hints when you have a better idea of where an object's users are likely to be.
You are trading infrastructure for constraints
Durable Objects do not remove complexity for free.
One object gives you one coordination point. That works very well when your application naturally divides into rooms, documents, matches, sessions or projects.
one object per documentGood fit.
one object for the entire applicationProbably not.
You are also choosing Cloudflare's runtime and APIs. A Redis based architecture can run in many places. Durable Objects cannot.
And Durable Objects are not a replacement for Kafka. If events need long term retention, replay, analytics pipelines or many independent backend consumers, a proper event system still solves a different problem.
But collaborative UI often does not need all of that.
Sometimes the requirement really is just:
Everyone looking at this thing needs to agree on what is happening.
Giving that thing one coordinator can remove a surprising amount of infrastructure.