Building Real-Time Applications with Durable Objects and WebSockets: A Friendly Guide

Author: Cristian González /

Dev Backend Websocket Cloudflare D1 Cloudflare Workers Cloudflare Durable Objects

Introduction

Have you ever wanted to build a real-time application like a chat app or live game but worried about the technical complexity? Cloudflare’s Durable Objects paired with WebSockets offers a surprisingly accessible way to create persistent, real-time connections. In this article, we’ll explore how to use these technologies to build interactive applications without diving too deep into technical waters.

What are Durable Objects and WebSockets?

Durable Objects are like digital containers that:

  • Remember information between user visits
  • Live on Cloudflare’s global network
  • Can handle thousands of simultaneous connections
  • Automatically “sleep” when not in use (hibernation)

WebSockets create a two-way communication channel that:

  • Stays open, unlike regular web requests
  • Allows instant updates in both directions
  • Works perfectly with modern browsers and mobile apps

Why This Combination Works So Well

When you pair Durable Objects with WebSockets, you get:

  • Low latency - messages arrive in milliseconds, not seconds
  • Cost efficiency - objects hibernate when not in use
  • Global reach - your app works worldwide without complex setup
  • Simplicity - no need to manage servers or databases

Building a Simple Chat Application

Let’s create a basic real-time chat that demonstrates these concepts:

1. The Server Side (Durable Object)

import { Env } from 'hono'
import { DurableObject } from 'cloudflare:workers'

export class ChatRoom extends DurableObject {
  // Keep track of connected users
  connections = new Map()

  constructor(ctx: DurableObjectState, env: Env) {
    // This is reset whenever the constructor runs because
    // regular WebSockets do not survive Durable Object resets.
    //
    // WebSockets accepted via the Hibernation API can survive
    // a certain type of eviction, but we will not cover that here.
    super(ctx, env)
  }

  // Handle new connections
  async fetch(request) {
    // Create a WebSocket pair - one for the client, one for the server
    const pair = new WebSocketPair()
    const [client, server] = Object.values(pair)

    // Accept the WebSocket connection with hibernation enabled
    this.ctx.acceptWebSocket(server)

    // Send back the client WebSocket
    return new Response(null, {
      status: 101,
      webSocket: client
    })
  }

  // When a new WebSocket connects
  webSocketOpen(ws) {
    // Store the connection
    this.connections.set(ws, { name: 'Anonymous' })

    // Send welcome message
    ws.send(
      JSON.stringify({
        type: 'SYSTEM',
        message: 'Welcome to the chat room!'
      })
    )
  }

  // When a message is received
  webSocketMessage(ws, message) {
    // Parse the message
    const data = JSON.parse(message)

    // Broadcast to all connections
    for (const connection of this.connections.keys()) {
      if (connection !== ws) {
        // Don't send back to sender
        connection.send(
          JSON.stringify({
            type: 'MESSAGE',
            user: this.connections.get(ws).name,
            message: data.message
          })
        )
      }
    }
  }

  // When a connection closes
  webSocketClose(ws) {
    // Remove the connection
    this.connections.delete(ws)
  }
}

2. The Client Side

// Connect to our chat room
async function connectToChat() {
  const socket = new WebSocket('wss://your-worker.example.dev/chat')

  // Handle incoming messages
  socket.addEventListener('message', (event) => {
    const data = JSON.parse(event.data)

    // Add message to chat display
    const chatBox = document.getElementById('chat-messages')
    const messageEl = document.createElement('div')

    if (data.type === 'SYSTEM') {
      messageEl.className = 'system-message'
      messageEl.textContent = data.message
    } else {
      messageEl.className = 'user-message'
      messageEl.textContent = `${data.user}: ${data.message}`
    }

    chatBox.appendChild(messageEl)
  })

  return socket
}

// Send a message
function sendMessage(socket, message) {
  if (socket.readyState === WebSocket.OPEN) {
    socket.send(JSON.stringify({ message }))
  }
}

// Connect when page loads
let chatSocket
window.addEventListener('load', async () => {
  chatSocket = await connectToChat()

  // Set up the send button
  document.getElementById('send-button').addEventListener('click', () => {
    const input = document.getElementById('message-input')
    sendMessage(chatSocket, input.value)
    input.value = ''
  })
})

Understanding Hibernation

One of the coolest features is hibernation, which works like this:

  1. When your chat room is active, the Durable Object is running
  2. If no one sends messages for a while, Cloudflare puts it to “sleep”
  3. When someone connects again, it “wakes up” automatically
  4. Your data is preserved, but you don’t pay for idle time

Hibernation happens automatically when you use this.ctx.acceptWebSocket(server), so you don’t need special code to enable it.

Benefits for Real Applications

  • Dating apps: Match participants can chat without delay
  • Multiplayer games: Game state updates instantly for all players
  • Collaborative tools: See other people’s changes as they happen
  • Live dashboards: Update metrics in real-time without page refreshes

Simple Deployment

To deploy your application:

# Install Wrangler (Cloudflare's CLI tool)
npm install -g wrangler

# Log in to Cloudflare
wrangler login

# Create a new project
wrangler generate my-realtime-app

# Deploy your code
wrangler publish

Conclusion

Building real-time applications used to require complex infrastructure and deep technical knowledge. With Durable Objects and WebSockets, you can create responsive, global applications with remarkably little code. The hibernation feature ensures you’re only paying for what you use, making this approach accessible even for small projects and startups.