Cloudflare Durable Objects Game Servers in 13 Steps [2026]

Most multiplayer game tutorials start with a VM, a Docker image, and a region picker. This one skips all three. Cloudflare’s Durable Objects let you write a single class that becomes a stateful, addressable “room” running at the edge, reachable by WebSocket, with no server to patch and no idle VM burning money while the lobby sits empty. Cloudflare has already shipped two public proofs of this: a multiplayer build of Doom running inside Workers and a 3D demo called Durable World, both using Durable Objects as the authoritative backend.

This tutorial builds a working real-time multiplayer game server from scratch using Cloudflare Durable Objects, Workers, and the WebSocket Hibernation API. By the end you will have a deployed room-based backend that tracks player positions, broadcasts state at a fixed tick rate, survives idle hibernation without racking up compute charges, and can be tested from a browser in minutes. Total setup time runs about 90 minutes if you already have a Cloudflare account, longer if you are starting from zero. We also compare this approach against three other current options for game backends: Colyseus, Edgegap, and Google Cloud Run, so you can decide which one actually fits your project before you commit a weekend to it. If you have already looked at a self-hosted game server framework like our guide on Nakama game server setup, this tutorial is useful as a lighter-weight alternative for teams who do not want to manage a server process at all.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

What Cloudflare Durable Objects Are and Why They Fit Multiplayer Games

A Durable Object is a single-instance, stateful compute unit that runs on Cloudflare’s network. Unlike a normal Worker, which is stateless and can spin up anywhere, a Durable Object has one canonical instance for a given ID, anywhere in the world. That single-instance guarantee is exactly what multiplayer games need for room state: when ten players connect to the same match, you want one authoritative place tracking positions, scores, and turn order, not ten competing copies racing to write the same value.

Cloudflare’s own engineering team leaned on this property when they built a proof of concept for networked Doom. As they explained in a company blog post, “Doom Multiplayer was the perfect example because it demonstrates the power of WebAssembly to run code in the browser and the power of Durable Objects to provide the backend for multiplayer games with low latency.” The same post describes the room model directly: “Each multiplayer game session creates a unique Id, or ‘room’, which corresponded to its own Durable Object instance.” That is the exact pattern this tutorial builds, just generalized so it works for any game loop, not just a WAD file.

Cloudflare followed that experiment with a second one aimed more directly at game engines. In a post about combining Workers, Durable Objects, and Unity, an engineer wrote: “I’ve heard a lot about the ability of Durable Objects and WebSockets to provide real-time consistency in applications, and to test that use case out, I’ve built Durable World: a simple 3D multiplayer world that is deployed entirely on our Cloudflare stack.” That post also notes plainly that “the server, which runs entirely on Cloudflare Workers, can be interacted with via WebSockets, and uses Durable Objects to manage game state.” Both posts, linked in the official Durable Objects documentation, back up what this tutorial sets out to prove: you can run a real multiplayer loop without owning a single server.

What makes this different from a Kubernetes-based approach like Agones on Kubernetes or a managed fleet like Amazon GameLift is that there is no cluster to provision and no fleet to keep warm. Each room is created on first contact, runs on whichever Cloudflare location is closest to the first player who connects, and goes dormant the moment nobody is sending traffic. For turn-based games, card games, lightweight real-time arenas, chat-driven lobbies, and casual multiplayer, that tradeoff is close to ideal. For twitch-reflex shooters needing sub-20ms tick precision across dozens of concurrent players, a dedicated server orchestrator is still the safer bet, and we will cover exactly where that line sits later in this guide.

Prerequisites: Accounts, Tools, and Versions

Before starting, get the following in place. None of this requires a credit card to begin, since Durable Objects are available on Cloudflare’s free Workers plan, though you will want the paid plan before shipping to real players.

  • A free Cloudflare account, created at dash.cloudflare.com.
  • Node.js 20 LTS or newer installed locally (Node 22 is the current LTS line as of late 2026).
  • npm 10+ or an equivalent package manager (pnpm, yarn).
  • Wrangler, Cloudflare’s CLI, installed via npm (the tutorial uses the current major Wrangler release line; run npx wrangler --version after install to confirm).
  • A code editor with TypeScript support (VS Code or Cursor both work fine).
  • Basic familiarity with WebSockets and async JavaScript/TypeScript.
  • Optional: a Unity or browser-based game client if you want to connect a real game rather than a test page.

You do not need to pre-provision any infrastructure. Cloudflare creates Durable Object namespaces and SQLite storage automatically the first time you deploy, based on the migrations block in your configuration file, which we cover in Step 4 below.

Step-by-Step: Setting Up the Project and the GameRoom Worker

Step 1: Create a Cloudflare Account and Verify Access

Sign up for a free Cloudflare account if you do not already have one. No domain is required to deploy a Worker. Once logged into the dashboard, open the Workers & Pages section once to confirm your account is active. This takes under five minutes and is the only manual dashboard step in the entire tutorial. Everything else happens from the terminal.

Step 2: Install Node.js and Wrangler

Confirm Node is installed, then scaffold a new Worker project using Cloudflare’s official starter command. This pulls the latest project template and installs Wrangler as a local dev dependency, so you are not locked to a globally installed version that can drift out of date.

node --version
npm create cloudflare@latest game-backend -- --type=hello-world
cd game-backend
npm install
npx wrangler --version
npx wrangler login

wrangler login opens a browser window to authorize the CLI against your Cloudflare account. Once authorized, every subsequent wrangler deploy in this project ships straight to your account without further prompts.

Step 3: Configure the Durable Object Binding

Open the generated configuration file (wrangler.toml or wrangler.jsonc, depending on your template version) and add a Durable Object binding plus a migration entry. The migration tells Cloudflare to create a new SQLite-backed Durable Object class on your next deploy. SQLite-backed Durable Objects are the recommended, and on the free plan the only available, storage backend as of this writing.

name = "multiplayer-game-backend"
main = "src/index.ts"
compatibility_date = "2026-09-01"

[[durable_objects.bindings]]
name = "GAME_ROOM"
class_name = "GameRoom"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["GameRoom"]

The compatibility_date field matters more than it looks. Older compatibility dates can silently disable newer runtime features, including parts of the WebSocket Hibernation API used two steps from now. Keep it current, and re-check it whenever Cloudflare ships a new runtime behavior you want to opt into.

Step 4: Define the GameRoom Durable Object Class

Create src/game-room.ts and define the class referenced in your binding. This is the authoritative room: it accepts WebSocket upgrade requests, tracks connected players in memory, and exposes handlers that Cloudflare’s runtime calls automatically on message, close, and error events.

import { DurableObject } from "cloudflare:workers";

interface PlayerState {
  x: number;
  y: number;
  vx: number;
  vy: number;
}

export class GameRoom extends DurableObject {
  players: Map<string, PlayerState> = new Map();

  async fetch(request: Request): Promise<Response> {
    if (request.headers.get("Upgrade") !== "websocket") {
      return new Response("Expected a WebSocket upgrade request", { status: 426 });
    }

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

    // acceptWebSocket (not accept()) enables hibernation support
    this.ctx.acceptWebSocket(server);

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

  async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
    const data = JSON.parse(message as string);
    if (data.type === "join") {
      this.players.set(data.id, { x: 0, y: 0, vx: 0, vy: 0 });
      await this.ctx.storage.setAlarm(Date.now() + 50);
    }
    if (data.type === "input") {
      const player = this.players.get(data.id);
      if (player) {
        player.vx = data.vx;
        player.vy = data.vy;
      }
    }
  }

  async webSocketClose(ws: WebSocket, code: number, reason: string) {
    for (const [id] of this.players) {
      this.players.delete(id);
    }
  }
}

Note the comment on acceptWebSocket. This single method call is the difference between a room that hibernates for free when idle and one that keeps billing compute duration around the clock. We cover the cost impact of this choice directly in the pricing section below.

Step 5: Add the Authoritative Tick Loop With Alarms

Durable Objects support a built-in alarm() handler, scheduled via ctx.storage.setAlarm(), which fires even after the object has gone dormant and been re-woken by the runtime. This is the mechanism for running a fixed-rate simulation tick without a dedicated background process. Add the following to your GameRoom class.

async alarm() {
  for (const [id, player] of this.players) {
    player.x += player.vx;
    player.y += player.vy;
  }

  const snapshot = Array.from(this.players.entries());
  this.broadcast({ type: "state", players: snapshot });

  if (this.players.size > 0) {
    // Re-schedule the next tick; ~20 ticks per second
    await this.ctx.storage.setAlarm(Date.now() + 50);
  }
}

broadcast(payload: unknown) {
  const message = JSON.stringify(payload);
  for (const ws of this.ctx.getWebSockets()) {
    ws.send(message);
  }
}

Notice the guard around re-scheduling: the alarm only re-arms itself if players are still present. An empty room with no scheduled alarm and no open WebSockets becomes fully eligible for hibernation, which is where the cost savings of this architecture actually materialize.

Step 6: Write the Entry Worker and Room Router

Your main Worker script is the public entry point. Its job is small on purpose: read a room name from the request, resolve it to a Durable Object ID, and forward the request to that specific object instance. Create src/index.ts with the following.

export { GameRoom } from "./game-room";

interface Env {
  GAME_ROOM: DurableObjectNamespace;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const roomName = url.searchParams.get("room") ?? "lobby";

    const id = env.GAME_ROOM.idFromName(roomName);
    const stub = env.GAME_ROOM.get(id);

    return stub.fetch(request);
  },
};

idFromName() deterministically maps any string, such as a match ID or lobby code, to the same Durable Object every time. Two players who both request ?room=arena-42 land in the exact same object instance, wherever in the world they happen to be connecting from.

Step-by-Step: Game Logic, Matchmaking, and Deployment

Step 7: Add Basic Matchmaking Logic

Real matchmaking usually needs more than a hardcoded room name. A common pattern is a lightweight matchmaking Worker that checks a small set of candidate rooms for open slots before handing the client a room name to connect to. Because Durable Objects can be queried via internal fetch calls, you can implement this without a separate database.

async function findOpenRoom(env: Env, maxPlayers = 8): Promise<string> {
  for (let i = 0; i < 20; i++) {
    const candidate = `arena-${i}`;
    const id = env.GAME_ROOM.idFromName(candidate);
    const stub = env.GAME_ROOM.get(id);

    const res = await stub.fetch("https://internal/status");
    const { playerCount } = await res.json();

    if (playerCount < maxPlayers) {
      return candidate;
    }
  }
  return `arena-${Date.now()}`; // overflow: spin up a fresh room
}

This requires a small internal status route inside GameRoom.fetch() that returns { playerCount: this.players.size } when the request path is /status, instead of attempting a WebSocket upgrade. For anything beyond casual skill-based matching, a dedicated matchmaking layer scales further than this simple scan, a tradeoff we also discuss in our walkthrough of Fly.io Machines game server setup.

Step 8: Add Rate Limiting to Protect the Tick Loop

Because a single Durable Object processes messages for every connected client, one abusive client flooding input events can degrade the room for everyone. Add a simple sliding-window limiter inside webSocketMessage.

const RATE_LIMIT = 30; // messages per 10-second window
rateBuckets: Map<WebSocket, number[]> = new Map();

async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
  const now = Date.now();
  const recent = (this.rateBuckets.get(ws) ?? []).filter((t) => now - t < 10_000);

  if (recent.length >= RATE_LIMIT) {
    ws.close(1008, "rate limit exceeded");
    return;
  }

  recent.push(now);
  this.rateBuckets.set(ws, recent);

  // ...existing message handling continues here
}

Step 9: Persist Critical State to Storage

In-memory class fields disappear if the Durable Object is evicted between requests. Scores, match results, and anything that must survive a restart belongs in the built-in SQLite storage API, accessed through this.ctx.storage. Writing is as simple as calling put(), and reads are near-instant since storage is colocated with the object itself.

async recordMatchResult(winnerId: string, finalScores: Record<string, number>) {
  await this.ctx.storage.put("lastMatch", {
    winnerId,
    finalScores,
    endedAt: Date.now(),
  });
}

async getLastMatch() {
  return await this.ctx.storage.get("lastMatch");
}

Step 10: Deploy to Cloudflare’s Network

With the Worker and Durable Object class written, deploy with a single command. Wrangler reads the migrations block, provisions the new SQLite-backed class, and publishes your Worker to Cloudflare’s global network in one step.

npx wrangler deploy

Expect output similar to this, with your own subdomain and a unique deployment version ID:

Total Upload: 4.82 KiB / gzip: 1.76 KiB
Uploaded multiplayer-game-backend (2.91 sec)
Deployed multiplayer-game-backend triggers (0.44 sec)
  https://multiplayer-game-backend.yoursubdomain.workers.dev
Current Version ID: 8f3a2e7c-91bd-4e12-9c04-5b7e21a9f001

Your game backend is now live at the printed URL, with no region selection, no load balancer configuration, and no idle fee while nobody is connected.

Step 11: Connect With a Location Hint for Latency-Sensitive Rooms

By default, a Durable Object is created near the location of whoever makes the first request to it. For predictable regional placement, for example pinning a tournament lobby to a specific continent, pass a location hint when creating the ID.

const id = env.GAME_ROOM.idFromName(roomName);
const stub = env.GAME_ROOM.get(id, { locationHint: "weur" }); // Western Europe

Step 12: Scale Beyond a Single Room

Each Durable Object handles one room, so scale comes from spreading load across many objects, not from making one object bigger. For a persistent world, shard geography into zones, with each zone backed by its own Durable Object instance, and hand players off between zone objects as they cross boundaries. For a lobby-based game, every match simply gets its own object via idFromName(), so horizontal scale is automatic: a thousand concurrent matches means a thousand separate Durable Object instances, each individually billed and individually hibernating when idle.

Step 13: Add Cross-Room Data With D1 or an External Database

A Durable Object’s SQLite storage is scoped to that single object. For a global leaderboard spanning every match, pair your rooms with a shared store, such as Cloudflare D1 or an external database, and have each room write results to it when a match ends, rather than trying to read state across objects directly, which Durable Objects do not support natively.

Testing Your Game Server With a Browser Client

You do not need a game engine to validate the backend. A plain HTML page with a WebSocket connection is enough to confirm the room, tick loop, and broadcast logic all work end to end.

const playerId = crypto.randomUUID();
const socket = new WebSocket(
  "wss://multiplayer-game-backend.yoursubdomain.workers.dev/?room=arena-1"
);

socket.onopen = () => {
  socket.send(JSON.stringify({ type: "join", id: playerId }));
};

socket.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (data.type === "state") {
    console.log("tick:", data.players);
  }
};

document.addEventListener("keydown", (e) => {
  const vx = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
  const vy = e.key === "ArrowDown" ? 1 : e.key === "ArrowUp" ? -1 : 0;
  socket.send(JSON.stringify({ type: "input", id: playerId, vx, vy }));
});

Open this page in two separate browser tabs, both pointed at the same room query parameter, and move one player with the arrow keys. If the setup is correct, both tabs log tick updates containing both players’ positions roughly 20 times per second, confirming the alarm-driven simulation loop and broadcast are functioning correctly. For a Unity client instead of a browser page, Cloudflare’s own Durable World example, linked earlier, demonstrates the native C# WebSocket client side of this same pattern.

Security, Authentication, and Anti-Cheat at the Edge

A Durable Object backend is only as trustworthy as the validation you put inside webSocketMessage. Nothing stops a modified client from sending impossible velocity values or claiming a score it never earned, so treat every incoming message as adversarial input, the same discipline any authoritative multiplayer architecture requires, whether it runs on Durable Objects, GameLift, or a bare-metal Agones cluster.

Three practical measures matter most. First, authenticate the player before the WebSocket upgrade completes, typically by validating a signed token passed as a query parameter or header, rather than trusting a client-supplied player ID. Second, clamp or reject movement and action values server-side, instead of broadcasting whatever the client claims. Third, use the rate limiter from Step 8 as a baseline defense against flooding, and consider closing connections that repeatedly send malformed JSON rather than silently ignoring them, since silent failures tend to mask both bugs and cheating attempts. None of this requires a separate security service. All of it runs inside the same Durable Object class you already wrote.

Monitoring, Logging, and Observability

Cloudflare exposes Durable Object metrics through its dashboard analytics, including invocation counts, duration, and errors per namespace, without any extra instrumentation. For development, wrangler tail streams live logs from your deployed Worker directly to your terminal, which is the fastest way to catch an unhandled exception in production without waiting on a dashboard refresh.

npx wrangler tail multiplayer-game-backend

For anything beyond basic tailing, route structured logs to an external sink using a Tail Worker or a logging binding, and track application-level metrics, such as players per room and average tick duration, as explicit log lines you can later feed into a dashboard. Durable Objects do not expose a built-in APM trace the way a container platform might, so this homegrown layer is worth building early rather than bolting on after a production incident.

Pricing Breakdown: What This Actually Costs at Scale

Durable Objects are available on both the Workers Free plan and the Workers Paid plan, with the free plan restricted to SQLite-backed classes, which is what this tutorial uses throughout. According to Cloudflare’s own Workers pricing documentation, the Paid plan carries a $5 per month minimum subscription charge per account, covering Workers, KV, Hyperdrive, and Durable Objects usage together, with clearly metered overage beyond the included allowances below.

Metered dimensionWorkers Free planWorkers Paid plan (included)Paid overage rate
Durable Object requests100,000 / day1 million / month$0.15 per million
Durable Object duration13,000 GB-seconds / day400,000 GB-seconds / month$12.50 per million GB-s
SQLite rows read5 million / day25 billion / month$0.001 per million rows
SQLite rows written100,000 / day50 million / month$1.00 per million rows
SQLite stored data5 GB total5 GB-month$0.20 per GB-month

The duration row is the one that separates a cheap game backend from an expensive one. Cloudflare bills duration as wall-clock time for as long as a Durable Object is active or idle in memory without being eligible for hibernation. A room that calls the older accept() method instead of acceptWebSocket() stays billed continuously for the entire connection, even while nobody sends a single message. Switching to the Hibernation API, as this tutorial does from Step 4 onward, lets an idle lobby stop accruing duration charges the moment its event handlers finish, which is the single biggest lever for keeping a Durable Objects game backend cheap at scale.

For context on what that means in practice, a casual multiplayer game with a few hundred concurrent rooms, each running short matches with real idle time between them, can plausibly stay within the $5 monthly base charge for weeks of testing. A persistent-world game with thousands of always-on zone objects ticking continuously will cross into the metered duration tier quickly, and should be budgeted accordingly before launch rather than discovered on the first invoice.

Cloudflare Durable Objects vs Colyseus vs Edgegap vs Cloud Run

Durable Objects are not the only path to a multiplayer backend, and they are not the right fit for every game. Three other current options cover most of the gap: Colyseus, a self-hostable Node.js framework built specifically for room-based multiplayer, Edgegap, an on-demand dedicated game server orchestrator, and Google Cloud Run, a general-purpose serverless container platform some teams use for lighter backend services like lobbies and matchmaking APIs.

PlatformHosting modelFree tierStarting paid priceBest fit
Cloudflare Durable ObjectsServerless, stateful edge compute100K DO requests/day, 13K GB-s/day$5/mo (Workers Paid) + usageWebSocket rooms, lobbies, turn-based and light real-time games
ColyseusSelf-hosted Node.js framework or managed cloudUnlimited self-hosted (bring your own server)$15/mo (Colyseus Cloud)Teams already on Node.js wanting room-based state sync without rolling their own
EdgegapOn-demand dedicated game server orchestrationFree trial, 1 app, 1.5 vCPU, 60-min uptime cap$0.00115 per vCPU-minute, pay-per-useAuthoritative Unity/Unreal server binaries needing global reach without idle cost
Google Cloud RunServerless containers180,000 vCPU-s and 2M requests/mo$0.000018 per vCPU-secondStateless APIs, matchmaking services, containerized backend logic

Colyseus, now in its 0.18 release line according to its GitHub releases, is the closest conceptual sibling to this tutorial’s room pattern, since Colyseus rooms also centralize state for a group of connected clients. The practical difference is where that room runs: Colyseus rooms run on servers you manage or on Colyseus Cloud, which starts at $15 per month with no CCU, DAU, or MAU limits, while Durable Objects rooms run inside Cloudflare’s own edge network with no server to manage at all. Teams with an existing Node.js game server codebase often find Colyseus faster to adopt, since it is purpose-built for games with matchmaking, state synchronization, and schema serialization baked in, whereas Durable Objects hand you a lower-level primitive you compose yourself, as this tutorial just did by hand.

Edgegap solves a different problem: running your own compiled dedicated server binary, built in Unity or Unreal, across more than 615 edge locations without paying for idle capacity, at $0.00115 per vCPU-minute with no charge while a deployment sits idle. That is the right tool when your simulation logic already exists as native server code and porting it to a JavaScript/TypeScript Durable Object is not realistic. For teams that have not written a dedicated server binary and are building from scratch in JavaScript, Durable Objects remove that entire deployment layer.

Google Cloud Run, meanwhile, rarely competes directly as the real-time game loop itself. Its free tier of 180,000 vCPU-seconds and 2 million requests per month makes it a reasonable home for the stateless pieces around a game, like a matchmaking API or a profile service, but Cloud Run containers do not hold the kind of single-instance, in-memory authoritative state that a game room needs without extra plumbing. Several teams pair Cloud Run for APIs with a dedicated, stateful layer, the same way our breakdown of CDN costs for game delivery covers edge costs for a different layer of the same stack. For the real-time room itself, Durable Objects, Colyseus, or Edgegap all beat a plain Cloud Run container on latency and state handling.

Common Pitfalls When Building Multiplayer Backends on Durable Objects

  • Calling accept() instead of acceptWebSocket(). This disables hibernation entirely, meaning the Durable Object keeps billing duration for the full life of the connection, even when the room is empty of activity.
  • Routing every player into one global Durable Object. A single object processes requests one at a time internally. Funneling your entire player base into one shared room, instead of one object per match, creates an artificial bottleneck that has nothing to do with Cloudflare’s actual capacity.
  • Forgetting the migrations block. Adding a new Durable Object class without a corresponding entry in [[migrations]] causes the deploy to fail or the binding to silently resolve to nothing, which looks like a networking bug but is a configuration miss.
  • Using a stale compatibility_date. Features like the WebSocket Hibernation API depend on runtime behavior gated by compatibility date. An old date can quietly disable capabilities this tutorial relies on.
  • Storing unbounded history in SQLite storage. Logging every tick or every chat message forever crosses from the free allowance into metered row-write and storage charges without warning, since there is no automatic expiry.
  • Assuming automatic multi-region replication. A Durable Object is pinned to one location once created. There is no built-in mechanism that copies a room’s live state to a second region if players scatter globally mid-match.
  • Trusting client-sent position and score data. Treating incoming WebSocket messages as ground truth instead of validating them server-side opens the door to the exact class of cheating an authoritative backend exists to prevent.
  • Not handling webSocketClose and webSocketError. Skipping cleanup on disconnect leaves stale entries in your in-memory player map, which grows unbounded across reconnect-heavy sessions like mobile games with flaky connections.

Troubleshooting Guide

SymptomLikely causeFix
Deploy fails with a Durable Object class errorMissing or mismatched [[migrations]] entryAdd a migrations tag listing the class in new_sqlite_classes
WebSocket connects then closes immediatelyServer called accept() or returned the wrong status codeCall ctx.acceptWebSocket(server) and return status 101 with the client socket
Players in different matches see each other’s stateDuplicate or reused room name passed to idFromName()Generate a unique room identifier per match, not a shared default
Monthly bill is higher than expectedRooms never hibernate because handlers never resolve or accept() is usedConfirm acceptWebSocket usage and check that alarms stop re-arming when a room is empty
Local wrangler dev works but production WebSocket failsA proxy, firewall rule, or missing Upgrade header in front of the WorkerTest with wrangler dev –remote and inspect headers reaching the Worker
Player state resets unexpectedlyRelying on in-memory class fields instead of persisted storagePersist critical state through ctx.storage.put(), not just class properties
Players far from the object’s region report high latencyDurable Object location was fixed near the very first connecting clientPass a locationHint when calling get() to pin placement intentionally
Requests start returning 429 or getting throttled during a load testFree plan’s 100,000 requests/day limit was exceededUpgrade to the Workers Paid plan or reduce client polling frequency
Tick loop silently stops after a whilealarm() handler did not call setAlarm() again before returningAlways re-schedule the next alarm at the end of the handler while players remain
TypeScript errors on DurableObject or WebSocketPair typesMissing or outdated @cloudflare/workers-types packageRun npm install @cloudflare/workers-types@latest and regenerate types

Advanced Tips for Production Game Backends

Once the base loop works, a few refinements separate a demo from something that survives real traffic. Use location hints deliberately for any room type where latency matters most, such as a competitive ranked match, while leaving casual lobbies to default placement, since forcing every room through a hint adds routing overhead you do not always need.

Shard aggressively rather than scaling a single object vertically. A persistent open world should be split into many small zone-based Durable Objects rather than one object trying to track every player on the map, both because a single object processes its internal work serially and because smaller objects hibernate more often, which keeps duration costs down. Batch SQLite writes where possible, since each put() call counts toward the metered row-write allowance, and writing a full snapshot once per second costs far less than writing on every tick. Finally, treat the Hibernation API as the default, not an optimization: write every new Durable Object class against acceptWebSocket() from day one, since migrating a live class off accept() later means auditing every code path that assumed the connection would stay open continuously.

Complete Working Project: What You’ve Built

At this point you have a deployed, edge-hosted multiplayer game backend consisting of an entry Worker that routes requests to named rooms, a GameRoom Durable Object class that accepts hibernatable WebSocket connections, an alarm-driven tick loop running an authoritative simulation at a fixed rate, basic matchmaking that scans for open rooms before creating new ones, rate limiting to protect the tick loop from abusive clients, and persistent match results stored through the built-in SQLite storage API. The entire stack deploys with one command, runs on Cloudflare’s global network without a region picker, and costs nothing beyond the free tier until your traffic genuinely outgrows it. From here, the natural next steps are wiring in real authentication, connecting a production game client in Unity or a browser framework, and deciding, using the comparison table above, whether any of your heavier simulation logic belongs on a dedicated orchestrator like Edgegap instead. For more cloud infrastructure patterns relevant to live-service games, see our broader cloud computing coverage.

Frequently Asked Questions

What are Cloudflare Durable Objects, in plain terms?

A Durable Object is a single-instance, stateful compute object that runs on Cloudflare’s network, reachable by a deterministic ID. For multiplayer games, each match or lobby maps to one Durable Object instance, which becomes the authoritative source of truth for everyone connected to it.

Do I need a Cloudflare paid plan to build a multiplayer game backend?

No. Durable Objects are available on the free Workers plan, limited to the SQLite storage backend, with 100,000 requests and 13,000 GB-seconds of duration included per day. That is enough for development and small-scale testing. A production launch with meaningful concurrent traffic will typically need the $5-per-month Workers Paid plan.

How many players can one Durable Object handle?

There is no hard player cap published by Cloudflare, but because a single object processes messages serially, practical capacity depends on how much work each message triggers. Most room-based games comfortably handle dozens of concurrent players per object, while very large counts in one room are better served by sharding into multiple objects.

Is this a replacement for Unity or Unreal’s built-in networking?

It replaces the backend hosting and room-state layer, not the client-side rendering or input handling your game engine still does. You still write client code in Unity, Unreal, or the browser, and that client talks to the Durable Object backend over WebSockets, the same way it would talk to any other authoritative server.

How does WebSocket Hibernation affect my Cloudflare bill?

Durable Objects are billed for duration while active or idle-but-ineligible for hibernation. Using acceptWebSocket() instead of the older accept() method lets a room stop accruing duration charges once its event handlers finish, so an idle lobby with no messages and no scheduled alarm effectively costs nothing in duration until the next connection or tick arrives.

Can I use Durable Objects alongside Colyseus or Edgegap?

Yes, and some teams do exactly that. A common split is running lightweight lobbies or turn-based modes on Durable Objects while routing performance-critical real-time modes through a dedicated orchestrator like Edgegap, with a shared account or matchmaking layer in front of both.

What happens if a Durable Object crashes mid-game?

Cloudflare automatically restarts the object on the next incoming request or scheduled alarm. Anything you persisted through ctx.storage survives the restart. Anything held only in in-memory class fields, like unsaved player positions, does not, which is why Step 9 of this tutorial separates critical state from transient in-memory state.

Is SQLite storage inside Durable Objects fast enough for real-time games?

For periodic writes, like saving a match result or a checkpoint every few seconds, yes, since storage is colocated with the object itself. For writing every single simulation tick at 20 or 60 times per second, batch writes rather than writing on every tick, both for performance and to stay comfortably inside the metered row-write allowance covered in the pricing section above.

Related Coverage

Elias Virtanen

Elias Virtanen

Cybersecurity Analyst

Elias Virtanen is the Cybersecurity Analyst at Tech Insider, bringing hands-on expertise from his background in penetration testing and security consulting. He previously worked as a security researcher at F-Secure in Helsinki, where he focused on threat intelligence and vulnerability disclosure. Elias covers ransomware trends, zero-trust architecture, and the evolving regulatory landscape including NIS2 and the EU Cyber Resilience Act. He holds a CISSP certification and an MSc in Information Security from Aalto University.

View all articles