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.

PartyKit is fundamentally a thin, ergonomic layer over Cloudflare’s Durable Objects primitive. Understanding how Durable Objects work — and how PartyKit maps its abstractions onto them — makes it straightforward to reason about costs, consistency, and the execution model of your real-time application.

Durable Objects: stateful compute at the edge

A Cloudflare Durable Object (DO) is a single-threaded, stateful JavaScript class instance that lives somewhere on Cloudflare’s global network. Unlike a stateless Worker, a Durable Object:
  • Holds in-memory state between requests for as long as it remains active.
  • Owns a co-located SQLite database accessible through this.ctx.storage, offering strongly consistent reads and writes without a round-trip to an external database.
  • Processes one concurrent request at a time via an internal input gate (though I/O can happen in parallel inside async code).
  • Hibernates automatically when idle, evicting the in-memory instance while keeping persistent storage intact; the next incoming request cold-starts the DO transparently.
  • Is globally distributed — a named DO always resolves to the same single instance worldwide, ensuring consistent shared state regardless of which Cloudflare data center the client request enters.
These properties make Durable Objects an ideal substrate for stateful rooms: each room is a single consistent point of truth, co-located with its storage, and billed only for the compute it actually uses.

The room-based model

PartyServer maps the concept of a “room” directly onto a Durable Object instance. Every unique combination of (server class, room name) corresponds to exactly one running DO instance. When two clients connect to room: "game-42" they are both reaching the same instance, so broadcasting between them is a direct in-process iteration — no pub/sub bus or external broker required. Room names are resolved via Cloudflare’s idFromName(name) API, which deterministically produces the same Durable Object ID for a given name string. This means:
  • Rooms are created lazily — no setup step is needed before the first client connects.
  • The same room name always maps to the same DO instance, everywhere on the network.
  • this.name inside a Server subclass always returns the room name that was used to address the instance.

Request routing with routePartykitRequest

routePartykitRequest is the glue between your Worker’s fetch handler and your Server subclasses. It inspects the incoming request URL for the pattern:
/<prefix>/:server/:name
where prefix defaults to "parties", :server is the kebab-cased Durable Object binding name, and :name is the room name. For example:
/parties/my-server/game-42
routes to a MyServer Durable Object instance named "game-42". The function performs this mapping by iterating over all DO namespace bindings in env, kebab-casing their keys, and matching against the :server segment. If a match is found, it:
  1. Resolves the DO instance via idFromName(name).
  2. Forwards the request (including WebSocket upgrade headers) to that instance’s fetch method.
  3. Returns null for unmatched paths, letting your fallback handler respond normally.
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    return (
      (await routePartykitRequest(request, env)) ||
      new Response("Not Found", { status: 404 })
    );
  },
} satisfies ExportedHandler<Env>;
routePartykitRequest also accepts an options object with onBeforeConnect and onBeforeRequest hooks for request interception (e.g. authentication), a cors option for automatic CORS header injection, and routingRetry for resilience against transient Durable Object infrastructure errors.

Connection lifecycle

When a WebSocket client connects to a room, PartyServer drives it through a well-defined lifecycle. Each stage is an overridable method on your Server class:
HookWhen it fires
onStart(props?)Once, when the DO first starts or resumes from hibernation. Use for storage initialization or remote data fetching.
onConnect(connection, ctx)Each time a new WebSocket connection is established. ctx.request holds the original HTTP upgrade request.
onMessage(connection, message)Each time a message arrives from a connected client.
onClose(connection, code, reason, wasClean)When a client closes its connection. The socket is already closed; use this for cleanup only.
onError(connection, error)When a transport error occurs on a connection.
onRequest(request)When a plain HTTP (non-WebSocket) request is made to the same DO.
onAlarm()When a Durable Object alarm fires. Schedule alarms with this.ctx.storage.setAlarm(date).
getConnectionTags(connection, ctx)Returns an array of string tags for a connection at connect time. Tags can be used to filter connections via getConnections(tag). Each connection supports up to 9 tags, max 256 characters each.
All hooks can be synchronous or async. PartyServer awaits async hooks before processing the next event.

Connection object

The connection argument passed to onConnect, onMessage, onClose, and onError is a standard WebSocket extended with:
  • connection.id — a unique identifier for this connection (generated via nanoid if not supplied by the client via ?_pk= query param).
  • connection.server — the room name this connection belongs to.
  • connection.state — up to 2 KB of arbitrary JSON state, readable and writable via connection.setState(). State survives hibernation cycles.
  • connection.tags — an array of string tags assigned by getConnectionTags.

Hibernation

Cloudflare’s WebSocket Hibernation API allows a Durable Object to suspend its in-memory state while keeping WebSocket connections alive at the infrastructure level. When a message arrives, the runtime cold-starts the DO, re-runs onStart, and delivers the message to onMessage — all transparently. Without hibernation, a DO with open WebSocket connections stays resident in memory indefinitely, accumulating wall-clock duration charges for every idle second. With hibernation, the DO is only charged for the time it is actually executing code.
Hibernation is opt-in. It is disabled by default. Enable it by setting a static options property on your Server subclass:
export class MyServer extends Server {
  static options = {
    hibernate: true,
  };

  onMessage(connection, message) {
    this.broadcast(message, [connection.id]);
  }
}
Subclasses inherit the nearest explicitly configured hibernate value from their prototype chain — you only need to set it once on a base class.
With hibernation enabled, PartyServer routes incoming WebSocket events through the Durable Object’s webSocketMessage, webSocketClose, and webSocketError entry points rather than keeping event listeners attached in memory. Your lifecycle hooks (onMessage, onClose, onError) are called identically in both modes, so you can enable or disable hibernation without changing application logic.

The actor model analogy

Durable Objects are conceptually similar to the actor model familiar from Erlang/Elixir and the GenServer abstraction. Each Durable Object instance is an isolated “actor” that:
  • Processes messages sequentially (no concurrent mutation of shared state).
  • Maintains its own private state.
  • Communicates with the outside world via requests (analogous to message passing).
PartyServer models these actors as “servers” rather than generic actors, because a Durable Object instance is inherently long-lived and stateful, making the server metaphor a natural fit. One important difference from Erlang actors: there is no terminate callback, because a Durable Object can be evicted by the runtime at any time without a deterministic shutdown signal. Use onAlarm to schedule deferred cleanup work instead.

Putting it all together

Browser                 Cloudflare Worker            Durable Object
  │                          │                              │
  │  GET /parties/my-server/ │                              │
  │       game-42 (WS)       │                              │
  │─────────────────────────▶│                              │
  │                          │  routePartykitRequest        │
  │                          │  idFromName("game-42")       │
  │                          │─────────────────────────────▶│
  │                          │                         onStart()
  │                          │                         onConnect()
  │◀─────────────────────────│◀─────────────────────────────│
  │       101 Switching      │                              │
  │                          │                              │
  │  send("hello")           │                              │
  │─────────────────────────────────────────────────────────▶
  │                          │                         onMessage()
  │                          │                         broadcast()
  │◀─────────────────────────────────────────────────────────
  │  receive("hello")        │                              │
Each step in the diagram corresponds directly to a lifecycle hook or a call to a PartyServer method, giving you complete observability over every stage of your application’s real-time communication.

Build docs developers (and LLMs) love