If you are building an application that requires some collaboration features or simply needs to feel “realtime”, you will end up having to use Websockets in some shape or form. One of the simplest ways of creating Websocket-powered services, without spinning up an “always-on” server, is using Cloudflare’s Durable Objects.
What are Durable Objects exactly?
In the land of serverless, we have long gotten used to stateless transactions: you send a request to an endpoint or a function, it processes the request and sends a response. Each time it is naïve: it has no memory of what happened before unless you store that data persistently.
However, if you’re building an application with any real-time features, like a game or a chatroom, some statefulness will be necessary. In your code you’d likely express that statefulness using some sort of class instance or object: const room = new Room()
- Durable Objects are exactly that, but offered as a hosted platform primitive.
This makes them ideal for workloads where some form of short-to-medium-term, in-memory state is necessary: collaborative features, CI/CD pipelines—and any time Websockets are in the mix. If you want to get a deeper perspective on Durable Objects, read this excellent piece from Lambros Petrou.
Similar to vanilla Cloudflare Workers, we can use the Durable Objects directly on the platform without any framework. However, if we want to build something a little more complex - we should use a routing framework. Hono, our tool of choice for today, features a robust middleware pattern that we can use to weave our Worker and Durable Object together.
Let’s get into it.
Pre-requisites
To go through this walkthrough you will need:
- A machine with
node
and your favorite JavaScript package manager installed. - If you want to ship your Durable Object-powered Worker to prod: a Cloudflare account and a paid plan.
Overview
We are going to build a simple Webhook inspection service, a similar/simplified version to webhook.site
or the one that is available in our own Fiberplane Studio. This service will:
- Allow any client to connect with it over a Websocket connection on route
/ws
- Listen for HTTP requests on route
/receiver-listen
- Any time a request is received on route
/receiver-listen
, serialize the method, header, and body data, and broadcast it over the existing pool of clients connected on/ws
In order to do that we will set up a basic Cloudflare Worker, powered by Hono, that will connect to a Durable Object instance and allow for Websocket connectivity from the client.
You will find all of the code from this article in this GitHub repository.
Walkthrough
Create a Cloudflare application
First let’s initialize a Cloudflare application using their own CLI and instruct it to use hono
as the web framework. Run the following command in your terminal:
Name it whatever you like, but we’re calling the project hooks-and-sockets
. Follow the CLI prompts to set up your new Cloudflare application.
Project structure
Here is an overview of the files that we will be working with:
Initial setup
In our index.ts
file let’s set up a basic Hono project.
Set up Durable Objects in wrangler.toml
To set up Durable Objects in wrangler.toml
, add the following configuration under the [durable_objects]
section:
This tells the wrangler
runtime to link Durable Object, an infrastructure component, with a TypeScript class WebhookReceiver
.
Based on the information in wrangler.toml
Cloudflare generates the correct type bindings in a global interface CloudflareBindings
, so that you can see what methods are available to you while working in your application. To regenerate the types run:
And inspect the worker-configuration.d.ts
file at the root of the repo.
Creating a basic Durable Object
Durable Objects are effectively “upgraded” Workers - they still need the Worker interface to communicate to the outside world, but they offer extra features that we mentioned earlier.
Continuing our “Durable Objects are just JavaScript/TypeScript classes” theme, starting one is as simple as:
We can then update our Hono-powered Worker to link the two together. First we need to add a line:
so that our Cloudflare runtime is aware of the newly-created Durable Object.
We can then define a new route that, when ping’ed, will forward the request details to the WebhookReceiver
. Here’s the updated src/index.ts
code:
Notice how we’re instantiating a WebhookReceiver
”stub” inside the /ws
handler. A “stub” is effectively a client Object that our Worker will use to communicate with the WebhookReceiver
.
If you now query your /ws
endpoint you should receive:
Adding Websockets
So far so good. However, we haven’t gone far from where we started - our /ws
route is still just a simple stateless request-response flow. Let’s upgrade it (see what I did there) to use websockets.
First, change the /ws
route and make sure it only accepts requests that ask to upgrade to use websockets. We’ll also use .idFromName()
and hardcode the passed in string parameter to "default"
instead of creating a new ID each time, to ensure that all open Websocket connections are connected to the same Durable Object. In real use cases, you will probably want to segment that in some way: E.g.: Pass in the ID of connected user, so they get their own Durable Object, along with their own pool of Websocket connections.
Websocket Hibernation API
Now in our WebhookReceiver
’s fetch
method let’s add some logic that will;
- Create a Websocket connection client-server pair
- Store the connection in a new Set
connections
- Tell the Durable Object to accept websocket messages
- and send the client information as a response.
Notice, however, that we’re not using the standard websocket.accept()
but Cloudflare’s acceptWebSocket()
. This method informs the client that it is ready to accept messages over the Websocket protocol while also allowing the Durable Object to “hibernate” and preserve memory when it is inactive, saving on costs.
The Hibernation API works by providing its own interface for Websocket handlers that we can use to trigger actions: webSocketMessage
, webSocketClose
, webSocketError
. Since our Durable Object will be waiting for most of the time and only taking action when a request is received on a different endpoint, we should really make use of this API.
In our application we don’t need to do much here as its main use of Websocket connectivity is to send messages to the client as opposed to receiving and acting on them, however we can add some logic to clean up our connections
Set if any of our Websocket connections close or error. We also don’t need to implement the standard Websocket “ping-pong” exchange as this is handled by Cloudflare.
Here’s what we have in our receiver.ts
so far:
Having both Worker and Durable Object in place, you can now try running the application (wrangler dev
) and connecting to the Websocket route /ws
with a Websocket client like websocat
: websocat --verbose ws://localhost:8787/ws
You should see a response like this indicating that the connection has been established succesfully:
Adding receiver listening route
Now that we have our basic Websocket connection working, let’s send some information down the wire. In our main Worker file src/index.ts
let’s add another route that will be our request listener: /receiver-listener/*
. Any time a request hits this route, we want to capture its information (method, path, and body), serialize it, and send it to each connected Websocket client.
This code will work but there is one thing we can improve here. In both routes we’re executing the same logic that creates the connection with the Durable Object. We can lift that into a middleware and essentially make it available to all routes at the same time. Here’s the updated code for src/index.ts
.
Websocket Hibernation gotchas
On paper this should all work, however, there’s one more gotcha here. Remember how we’re using Cloudflare’s Websocket Hibernation API? In practice what that means is that every time a Durable Object is “awakened” from its hibernation, its constructor
function gets called - i.e. our pool of client connections stored in the this.connections
Set effectively gets wiped clean.
Fortunately, Cloudflare’s runtime provides a way to retrieve all accepted Websocket connections in a getWebSockets()
method available on the same DurableObjectState
that we called acceptWebSocket()
on. In our constructor
we can call this.ctx.getWebSockets()
and re-populate our connections
Set.
Here’s our final receiver Durable Object:
Recap
Whew! This was a long one so here’s a quick recap what we have achieved:
- Set up a Cloudflare application using Hono framework
- Configured and created a basic Durable Object in
wrangler.toml
, calledWebhookReceiver
and updated our Worker to link with it - Added Websocket support to the
/ws
route and implemented Websocket connection handling in the Durable Object using Cloudflare’s Websocket Hibernation API - Created a
/receiver-listen
route to capture and broadcast requests to existing Websocket connections.
If everything went well, by the end of this you should have a small service that:
-
You can connect and establish a Websocket connection with on route
/ws
: -
Send a request against the
/receiver-listen
route and have the request details mirrored in the screen you’ve connected in the previous step: