DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Playwright for Real-Time Applications: Testing WebSockets and Live Data Streams
  • The AI Autonomy Spectrum: 7 Architecture Patterns for Intelligent Applications
  • Smart Deployment Strategies for Modern Applications
  • LLM Integration in Enterprise Applications: A Practical Guide

Trending

  • How to Save Money Using Custom LLMs for Specific Tasks
  • The Big Data Architecture Blueprint: Core Storage, Integration, and Governance Patterns
  • Spring AI Advisors: Chat Memory, Token Tracking, and Message Logging
  • How to Parse Large XML Files in PHP Without Running Out of Memory
  1. DZone
  2. Coding
  3. Languages
  4. The Serverless WebSocket: Building Real-Time Applications With Cloudflare, Hono, and Durable Objects

The Serverless WebSocket: Building Real-Time Applications With Cloudflare, Hono, and Durable Objects

Build serverless WebSockets with Cloudflare Workers for routing and Durable Objects for state. This creates a scalable, low-latency real-time app at the edge.

By 
Mayur Vekariya user avatar
Mayur Vekariya
·
Sep. 29, 25 · Analysis
Likes (1)
Comment
Save
Tweet
Share
3.4K Views

Join the DZone community and get the full member experience.

Join For Free

The demand for real-time applications has exploded, from collaborative documents and live data dashboards to multiplayer games and instant messaging. WebSockets, with their persistent, bi-directional communication protocol, have become the de facto standard for building these experiences. However, the traditional approach — running a dedicated server to manage thousands of long-lived connections — introduces significant complexities in scalability, cost, and operational overhead.

This paradigm is being fundamentally challenged by the rise of serverless computing. But can the stateless, ephemeral nature of typical serverless functions truly support a stateful, persistent protocol like WebSockets?

This article explores a unique and powerful answer to that question, demonstrating how to build a fully serverless WebSocket service using Cloudflare Workers, Durable Objects, and the lightweight Hono framework. By decoupling the routing from the stateful logic and leveraging the global Cloudflare network, we can create a solution that is not only highly scalable and cost-effective but also globally distributed by default.

The State-Full Dilemma in a Serverless World

Most serverless platforms are built on a "function-as-a-service" model where compute instances are stateless and short-lived. A function spins up to handle a single request and then shuts down, discarding all in-memory state. This design is perfect for REST APIs or event-driven tasks, but fundamentally clashes with the requirements of a WebSocket, which demands a continuous, open connection.

Attempting to run a WebSocket server on such a platform typically requires creative, often complex workarounds involving external, stateful databases or pub/sub services. These solutions reintroduce the very complexity and latency that serverless was meant to solve.

Cloudflare's platform, however, provides a game-changing primitive that bridges this gap: Durable Objects.

Cloudflare's Durable Objects: State at the Edge

A Durable Object is a single-instance class that provides a dedicated, stateful environment. Unlike a regular Cloudflare Worker, which is a stateless function, a Durable Object has a persistent identity. When a request is made to a specific Durable Object ID, Cloudflare guarantees that it will always be routed to the same single instance. This instance maintains its in-memory state for as long as it's needed, making it a perfect fit for managing WebSocket connections.

Here’s the architecture we’ll build:

  1. A Cloudflare Worker acts as the public-facing entry point, serving as a smart router.
  2. When a WebSocket connection request arrives, the Worker looks up a specific Durable Object instance (e.g., a "chat room" or a "game session").
  3. The Worker then forwards the WebSocket upgrade request to that specific Durable Object, which takes over the connection.

This separation of concerns allows the Worker to handle millions of concurrent routing requests efficiently, while the Durable Object focuses solely on managing the state and real-time communication for its unique session. A key benefit is the WebSocket Hibernation API, which allows the Durable Object to become inactive and save on compute costs when no messages are being sent, while still keeping the underlying connection alive.

Why Hono? The Framework for the Edge

To streamline development, we'll use Hono, a lightweight, fast web framework designed for the edge. Its API is familiar to developers coming from frameworks like Express.js, and it has first-class support for Cloudflare Workers and WebSockets. Hono’s small footprint and performance make it an ideal choice for a serverless environment where every millisecond counts.

Let’s dive into the code and build a simple, scalable chat room.

Step 1: Project Setup and Configuration

First, initialize your project using the Cloudflare CLI. This will scaffold a basic Hono worker.

Shell
 
npm create cloudflare@latest serverless-chat-app --framework=hono


Next, configure your wrangler.toml file. This is crucial as it informs Cloudflare about our Durable Object binding.

TOML
 
name = "serverless-chat-app"
main = "src/index.ts"
compatibility_date = "2024-05-18"
workers_dev = true

[[durable_objects.bindings]]
name = "CHAT_ROOM"
class_name = "ChatRoom"

[[migrations]]
tag = "v1"
new_classes = ["ChatRoom"]


This configuration creates a binding named CHAT_ROOM that maps to a class we'll define called ChatRoom. The migration tag is essential for Cloudflare to correctly manage the Durable Object lifecycle.

Step 2: The Stateful Durable Object

This is the core of our application. The ChatRoom class will hold all our connected WebSocket sessions in memory and manage the message broadcasting.

TypeScript
 
// src/chat-room.ts

// Define the structure of a chat message for type safety.
interface ChatMessage {
  user: string;
  message: string;
  timestamp: number;
}

export class ChatRoom {
  private state: DurableObjectState;
  private sessions: Set<WebSocket>;

  constructor(state: DurableObjectState) {
    this.state = state;
    this.sessions = new Set();
  }

  // Handles all incoming requests. We only care about WebSocket upgrades here.
  async fetch(request: Request): Promise<Response> {
    const upgradeHeader = request.headers.get("Upgrade");
    if (upgradeHeader !== "websocket") {
      return new Response("Expected a WebSocket upgrade request.", { status: 426 });
    }

    const { 0: client, 1: server } = new WebSocketPair();
    
    // Accept the connection and add the server-side WebSocket to our session list.
    this.state.acceptWebSocket(server);
    this.sessions.add(server);

    console.log(`New WebSocket connection established. Total connections: ${this.sessions.size}`);

    // Return the client-side WebSocket back to the client.
    return new Response(null, { status: 101, webSocket: client });
  }

  // Handles messages received from any connected client.
  async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
    if (typeof message === "string") {
      try {
        const parsedMessage: ChatMessage = JSON.parse(message);
        console.log(`Received message from ${parsedMessage.user}: ${parsedMessage.message}`);
        
        const response: ChatMessage = {
          user: parsedMessage.user,
          message: parsedMessage.message,
          timestamp: Date.now(),
        };
        const serializedResponse = JSON.stringify(response);

        // Broadcast the message to all other connected clients in the room.
        this.sessions.forEach((session) => {
          if (session !== ws) {
            session.send(serializedResponse);
          }
        });

      } catch (e) {
        console.error("Failed to parse message:", e);
      }
    }
  }

  // Cleans up a session when a WebSocket connection is closed.
  async webSocketClose(ws: WebSocket): Promise<void> {
    console.log("WebSocket connection closed.");
    this.sessions.delete(ws);
    console.log(`Total connections remaining: ${this.sessions.size}`);
  }

  // Handles errors on a WebSocket connection.
  async webSocketError(ws: WebSocket, error: any): Promise<void> {
    console.error("WebSocket error:", error);
    this.sessions.delete(ws);
  }
}


This class is the key to our serverless solution. It is stateful because the sessions set persists in memory for the lifetime of the Durable Object, allowing us to manage and broadcast messages to all connected clients effortlessly.

Step 3: The Hono Worker as a Router

The main index.ts file uses Hono to handle the incoming HTTP request. Its sole purpose is to get a handle on the correct Durable Object instance and pass the request off to it.

TypeScript
 
// src/index.ts

import { Hono } from "hono";
import { ChatRoom } from "./chat-room";

// Define the environment bindings, including our Durable Object.
type Bindings = {
  CHAT_ROOM: DurableObjectNamespace;
};

const app = new Hono<{ Bindings: Bindings }>();

// Define the WebSocket endpoint.
app.get("/ws", (c) => {
  // Use a hardcoded name to create a consistent Durable Object ID.
  // This ensures all clients connect to the same 'global-chat-room' instance.
  const objectId = c.env.CHAT_ROOM.idFromName("global-chat-room");
  const durableObjectStub = c.env.CHAT_ROOM.get(objectId);

  // Forward the original request to the Durable Object stub.
  return durableObjectStub.fetch(c.req.url, c.req);
});

// Export the ChatRoom class so Cloudflare can find it.
export { ChatRoom };

// Export the Hono app as the default handler for the Worker.
export default app;


The idFromName method is fundamental here. It generates a consistent Durable Object ID based on a string name. Any client connecting to /ws will always be routed to the same ChatRoom instance, creating a single, shared chat room for all users.

Step 4: The Client-Side Connection

The client-side code is a standard WebSocket implementation. It connects to the URL of our Cloudflare Worker and handles incoming and outgoing JSON messages.

HTML
 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Serverless Chat</title>
</head>
<body>
    <h1>Cloudflare Serverless Chat</h1>
    <input type="text" id="username" placeholder="Enter your name">
    <ul id="messages"></ul>
    <form id="message-form">
        <input type="text" id="message-input" placeholder="Type a message..." autocomplete="off">
        <button type="submit">Send</button>
    </form>
    <script>
        const usernameInput = document.getElementById("username");
        const messagesList = document.getElementById("messages");
        const messageForm = document.getElementById("message-form");
        const messageInput = document.getElementById("message-input");

        let ws;

        // Establish the WebSocket connection.
        function connect() {
            // Replace with your Cloudflare Worker URL
            const workerUrl = "wss://<your-worker-subdomain>.workers.dev/ws";
            ws = new WebSocket(workerUrl);

            ws.onopen = (event) => { console.log("Connected to WebSocket server."); };
            ws.onmessage = (event) => {
                const data = JSON.parse(event.data);
                const li = document.createElement("li");
                li.textContent = `${data.user}: ${data.message}`;
                messagesList.appendChild(li);
                messagesList.scrollTop = messagesList.scrollHeight;
            };
            ws.onclose = (event) => {
                console.log("Disconnected. Attempting to reconnect in 5 seconds...");
                setTimeout(connect, 5000);
            };
            ws.onerror = (error) => { console.error("WebSocket error:", error); ws.close(); };
        }

        // Handle sending a message.
        messageForm.addEventListener("submit", (e) => {
            e.preventDefault();
            const message = messageInput.value;
            const user = usernameInput.value || "Anonymous";
            if (message.trim() !== "" && ws && ws.readyState === WebSocket.OPEN) {
                const chatMessage = { user, message };
                ws.send(JSON.stringify(chatMessage));
                messageInput.value = "";
            }
        });
        connect();
    </script>
</body>
</html>


Conclusion

This architecture completely removes the need to manage a traditional WebSocket server. The complexity of scalability and state management is handled by the Cloudflare platform, while the developer is free to focus on the application logic. The result is a real-time service that is not only highly scalable and cost-effective but also globally distributed, thanks to Cloudflare's edge network. 

The combination of Cloudflare Workers, stateful Durable Objects, and the Hono framework provides a robust and elegant solution for building the next generation of real-time applications at the edge.

WebSocket applications Object (computer science)

Published at DZone with permission of Mayur Vekariya. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Playwright for Real-Time Applications: Testing WebSockets and Live Data Streams
  • The AI Autonomy Spectrum: 7 Architecture Patterns for Intelligent Applications
  • Smart Deployment Strategies for Modern Applications
  • LLM Integration in Enterprise Applications: A Practical Guide

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook