If you'd rather listen to this post, it was also published as a podcast episode.
Cloudflare’s Workers product is probably best known among developers as a competitor to Amazon Lambda that runs on Cloudflare’s edge, but Workers has a trick up its sleeve that has also quietly made it a formidable platform for realtime multiplayer apps.
I say quietly because many developers building on Workers might not even know that they are building on it, as Cloudflare Workers power a number of popular multiplayer infrastructure products:
Cloudflare's dominance in this niche comes from a feature called Durable Objects, which implements an abstraction that is both unique among cloud providers, and incredibly useful for real-time apps.
Analogous to the way that function-as-a-service (FaaS) platforms like Amazon Lambda run a developer-provided function in the cloud, Durable Objects run a developer-provided class.
These Durable Object classes implement a method for handling requests. This method is akin to the handler function provided to other FaaS platforms, but it differs in two ways that are important for multiplayer apps:
The other important difference from a traditional FaaS is this: Durable Objects provide a way to reliably connect two clients to the same object. Each object has a (string) name that is used when connecting to it, which typically corresponds to a “room” or “document ID” in the application itself.
In practice, this means that two clients can establish WebSocket connections using the same document ID, and the infrastructure will guarantee that they are connected to the same Durable Object instance. This object will then be the authority for the state of that document, and can directly send changes to every client.
This is the difference that makes Durable Objects so compelling for multiplayer use cases, but it's also the reason that the Durable Object abstraction hasn't been slapped on top of other FaaS platforms – it is a radical departure from core assumptions made by their scheduling and routing systems.
The best way to understand how much the Durable Object model simplifies multiplayer apps might be to compare it with Amazon's own preferred approach to using WebSockets on Lambda.
Lambda can't directly speak WebSocket. Instead, it needs to be paired with an API Gateway, which translates WebSocket messages and events into Lambda invocations. In other words, every single WebSocket message independently invokes a Lambda function.
These functions are stateless, but even something as simple as a broadcast channel needs state (to know which listeners to pass along the message to). Amazon resolves this by persisting handles to active connections in DynamoDB.
This means that every single message to a broadcast channel results in a DynamoDB query for a list of current clients, which it then iterates over to send messages to each.
This works, I guess. But I would need to make a spreadsheet model over three different AWS services to tell you what it cost, and it involves managing a bunch more AWS resources and writing a bunch of glue code to basically implement a context switch in userspace.
But there's a more subtle reason why this architecture doesn't work well for multiplayer: it lacks an authority. A bunch of multiplayer stuff gets way easier if, when processing a document change, a server is allowed to assume that it is the only server currently allowed to process changes for that document.
The desired abstraction, from the application programmer's point of view, is a single thread that can process all changes to one document in the order they are received. This is essentially what Durable Objects gives you.
For more depth, Sunil Pai of PartyKit wrote a great post explaining Durable Objects in the context of traditional FaaS.
Back in 2020, I wrote about the need for serverless infrastructure for WebSockets (wishing for “websocketd meets Lambda”). While Durable Objects has since partially filled that void, it comes with some caveats to my ideal:
workerd
runtime is open source, but the scheduling/routing pieces that would be needed to run a multi-node system are proprietary).One way to think about Jamsocket (the company I co-founded and the publisher of Browsertech) is as combining the scheduling/routing/one-instance-per-document functionality of Durable Objects with the universality of running a full Linux process (optionally on your own metal) instead of a V8 isolate.
Having seen the apps that can be built on stateful, scalable WebSocket tech, I expect to see a lot more apps take advantage of this architecture, whether on Durable Objects, Jamsocket, or something else. I'm biased, but it's pretty neat.
Until next time,
-- Paul