Websockets and Cloudflare Workers

February 22, 2025 / 4 minute read

Share this:

Websockets and Workers! A match made in heaven!

Or not.

Stateful, persistent communication? With ephemeral, floaty web workers that have almost no state?

How in the world can those interoperate!?

Leave it to the talented folks at Cloudflare to build a fantastic integration. This is way easier than you might expect!

Getting Started

To get set up, you want to create a standard boilerplate web worker. I love starting with the Application Starter:

wrangler init
# give it a name...
# choose "Application Starter"
# then "API starter (OpenAPI compliant)"

(If you don't have wrangler installed yet, then you need to go take care of that!)

Now we're set up! Let's get this thing rolling!

Get it working locally

As with most workers (and other projects) - the first step is to get things working locally. Thankfully, wrangler has a pretty killer setup for this. So let's get things started right out of the gate.

Navigate to the directory you just created and:

wrangler dev

Add websocket

Now we're going to add a simple websocket server to the endpoints directory

endpoints/websocket.ts

import { OpenAPIRoute } from "chanfana";
import { Context } from "hono";

export class WebSocket extends OpenAPIRoute {
    async handle(c: Context) {
        // optional - if you want to be strict
        const upgradeHeader = c.req.header("Upgrade");
        if (!upgradeHeader || upgradeHeader !== "websocket") {
            return new Response("Expected Upgrade: websocket", { status: 426 });
        }

        const webSocketPair = new WebSocketPair();
        const [client, server] = Object.values(webSocketPair);

        server.accept();
        server.addEventListener("message", (event) => {
            console.log(event);
            server.send("Hello from the other side!");
        });

        server.addEventListener("close", () => {
            console.log("WebSocket closed");
        });

        return new Response(null, {
            status: 101,
            webSocket: client,
        });
    }
}

And integrate into the index.ts file:

index.ts

import { WebSocket } from "./endpoints/websocket";

...

openapi.all("/api/ws", WebSocket);

Test it!

Now how do you test websockets!? Well there are some cool tools out there on the web.

But I just wanted to test locally, so I made a little python script to test with:

wstest.py

import argparse
from websocket import create_connection

def main():
    parser = argparse.ArgumentParser(description='WebSocket test client')
    parser.add_argument('--domain', '-d', nargs='?', default='localhost:8787', help='The hostname to connect to (e.g. localhost:8787)')
    parser.add_argument('--tls', '-t', action='store_true', help='Use TLS')
    parser.add_argument('--path', '-p', nargs='?', default='/api/ws', help='The path to connect to (e.g. /api/ws)')
    args = parser.parse_args()

    if args.tls:
        protocol = "wss"
    else:
        protocol = "ws"
    
    # ensure that args.domain does not have a protocol prefix
    # parse domain as a url
    domain = args.domain.split('://')[1]
    path = args.path

    # ensure that there is only one slash between domain and path
    if path.startswith('/'):
        path = path[1:]
    if domain.endswith('/'):
        domain = domain[:-1]

    ws_url = f"{protocol}://{domain}/{path}"
    ws = create_connection(ws_url)
    print(f"--> Connected to {ws_url}")
    print("--> Sending 'Hello, World'...")
    ws.send("Hello, World")
    result = ws.recv()
    print(f"<-- Received '{result}'")

    try:
        while True:
            msg = input('--> ')
            ws.send(msg)
            result = ws.recv()
            print(f"<-- Received '{result}'")
    except KeyboardInterrupt:
        print("\nClosing connection...")
    finally:
        ws.close()

if __name__ == '__main__':
    main()

You will need one little library:

pip install websocket-client

And then you should be good to test!

# use the port that your websocket worker is running on from `wrangler dev`
python3 ./wstest.py -d localhost:59462

Deploy to Cloudflare

This should be pretty straightforward! Deploy your worker!

wrangler deploy

Then redirect the websocket test script and see what happens! (Make sure to include the TLS flag!):

python3 ./wstest.py -d my-deployed-domain.workers.dev -t

More debugging

There are lots of snags you can hit with websockets, especially if you start using the browser.

  • Beware of CORS
  • Be mindful of ws:// vs. wss:// (for TLS connections) protocol issues
  • Be careful of client code, which needs to handle reconnections and can cause problems disconnecting unintentionally
  • Firewalls, enterprise security restrictions, and reverse proxies can cause all sorts of weird issues with websockets!
  • It is probably worth using socket.js or having some fallback mechanisms handled... but more on that another time.

Successful Websocketing!

Congratulations! You have hopefully deployed a websocket server successfully to Cloudflare!

I would love to hear how it went, if you ran into issues, how this article can be improved, or what you ended up building! Let me know!

Thanks!