Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/cloudflare/partykit/llms.txt

Use this file to discover all available pages before exploring further.

PartyServer maps cleanly onto the classic multiplayer model: each room becomes a single Durable Object instance with one authoritative copy of the game state. Every player connects to the same DO, the server applies updates and broadcasts deltas, and the WebSocket hibernation API keeps costs near zero even when thousands of rooms sit idle between moves.

Why Durable Objects fit multiplayer

A Durable Object is a single-threaded, location-aware, stateful compute unit. That means there is exactly one instance of your game room running at a time — no split-brain, no synchronization between replicas. PartyServer wraps that primitive with lifecycle hooks (onConnect, onMessage, onClose) and a broadcast helper so you can focus on game logic rather than infrastructure.

Single authoritative state

One DO instance per room = one source of truth. No distributed locks or conflict resolution needed.

Connection-scoped identity

Attach player metadata directly to the connection object with connection.setState().

Cheap at rest

Hibernation mode lets the DO sleep between messages, billing only for active compute time.

Location awareness

Pass a locationHint to routePartykitRequest to place a room close to its players.

Core patterns

Storing world state in the DO

Keep the authoritative state as an in-memory field on the server class. Because there is only one instance per room, reads are instant and writes never conflict.
// server.ts
import { Server, routePartykitRequest } from "partyserver";
import type { Connection } from "partyserver";
import { env } from "cloudflare:workers";

type Player = { id: string; x: number; y: number };

export class GameRoom extends Server {
  static options = { hibernate: true };

  // In-memory world state — one copy per room
  players = new Map<string, Player>();

  onConnect(connection: Connection) {
    // Send the current world state to the new player
    const snapshot = JSON.stringify({
      type: "snapshot",
      players: [...this.players.values()],
    });
    connection.send(snapshot);
  }

  onMessage(connection: Connection, message: string) {
    const data = JSON.parse(message);

    if (data.type === "move") {
      // Update this player's position
      this.players.set(connection.id, {
        id: connection.id,
        x: data.x,
        y: data.y,
      });

      // Broadcast the delta to every other player
      this.broadcast(
        JSON.stringify({ type: "move", id: connection.id, x: data.x, y: data.y }),
        [connection.id] // exclude the sender
      );
    }
  }

  onClose(connection: Connection) {
    // Clean up and notify remaining players
    this.players.delete(connection.id);
    this.broadcast(
      JSON.stringify({ type: "leave", id: connection.id }),
      [connection.id]
    );
  }
}

export default {
  async fetch(request: Request): Promise<Response> {
    return (
      (await routePartykitRequest(request, env)) ||
      new Response("Not Found", { status: 404 })
    );
  },
} satisfies ExportedHandler<Env>;

Tracking player identity via connection state

connection.setState() attaches up to 2 KB of arbitrary data to a connection. This data survives hibernation and is available in every subsequent lifecycle hook — ideal for player names, authentication tokens, or team assignments.
import type { Connection, ConnectionContext } from "partyserver";

type PlayerState = { name: string; team: "red" | "blue" };

onConnect(connection: Connection<PlayerState>, ctx: ConnectionContext) {
  const url = new URL(ctx.request.url);
  const name = url.searchParams.get("name") ?? "Anonymous";
  const team = (url.searchParams.get("team") as PlayerState["team"]) ?? "red";

  // Persist player identity on the connection itself
  connection.setState({ name, team });

  this.broadcast(
    JSON.stringify({ type: "join", id: connection.id, name, team }),
    [connection.id]
  );
}

Handling disconnects in onClose

When a player drops their connection, onClose is your chance to clean up their state and let the room know.
onClose(connection: Connection<PlayerState>) {
  const { name } = connection.state ?? { name: "Unknown" };

  this.players.delete(connection.id);

  this.broadcast(
    JSON.stringify({ type: "leave", id: connection.id, name }),
    [connection.id]
  );
}
By the time onClose is called, the connection is already closed and cannot receive messages. Only broadcast to other connections.

Persisting state between sessions

For games that should survive server evictions, persist critical state to Durable Object storage and reload it in onStart:
async onStart() {
  const stored = await this.ctx.storage.get<Player[]>("players");
  if (stored) {
    for (const p of stored) this.players.set(p.id, p);
  }
}

async onClose(connection: Connection) {
  this.players.delete(connection.id);
  await this.ctx.storage.put("players", [...this.players.values()]);
  this.broadcast(JSON.stringify({ type: "leave", id: connection.id }), [connection.id]);
}

Client setup with PartySocket

PartySocket is a drop-in WebSocket replacement with automatic reconnection and message buffering.
// client.ts
import { PartySocket } from "partysocket";

const socket = new PartySocket({
  host: "https://my-game.workers.dev", // defaults to window.location.host
  party: "game-room",                  // kebab-cased DO binding name
  room: "room-42",                     // room / DO instance name
  query: {
    name: "Alice",
    team: "blue",
  },
});

socket.addEventListener("message", (event) => {
  const data = JSON.parse(event.data as string);

  switch (data.type) {
    case "snapshot":
      renderWorld(data.players);
      break;
    case "move":
      updatePlayer(data.id, data.x, data.y);
      break;
    case "leave":
      removePlayer(data.id);
      break;
  }
});

// Send the local player's new position
function sendMove(x: number, y: number) {
  socket.send(JSON.stringify({ type: "move", x, y }));
}

Wrangler configuration

// wrangler.jsonc
{
  "name": "my-game",
  "main": "src/server.ts",
  "durable_objects": {
    "bindings": [
      {
        "name": "GameRoom",
        "class_name": "GameRoom"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["GameRoom"]
    }
  ]
}

Real-world examples

The PartyKit repository ships two fixtures that demonstrate these patterns at production scale:

tldraw fixture

A collaborative whiteboard using tldraw’s sync store backed by a PartyServer DO.

Globe fixture

A live globe that tracks every visitor’s geographic position using connection.setState() and Cloudflare geolocation headers.
The globe fixture in particular is a clean reference for the connect → state → broadcast → disconnect lifecycle: on connect it reads request.cf.latitude/longitude, persists that to conn.setState(), broadcasts a marker to all peers, and removes the marker in onClose.

Build docs developers (and LLMs) love