A Durable Object is a single-instance, addressable, stateful object — the thing you reach for when two requests must agree, and when the agreement has to survive both of them.
Three of them exist in the kit, and all three are in the game capabilities.
| Object | Capability | What it holds |
|---|---|---|
| A game session | multiplayer | The authoritative state no client can be trusted with |
| The queue | matchmaking | Waiting players, bucketed by region and skill |
| Presence | matchmaking | Every online player’s socket |
Why those three, and nothing else
The capabilities built on them are multiplayer and matchmaking; the other durability primitive is durable jobs.
Everything else in the kit is either a row in D1 or a durable job.
A Durable Object earns its place when serialization matters: a turn resolved against state two clients both hold a view of, or a queue where two players must not be matched to three different opponents. D1 answers what is true; a Durable Object answers what happens next, in order.
An audit trail does not need one. A leaderboard does not need one. A balance does not — the constraint on the table is stronger and cheaper.
Presence hibernates, and that is what makes it affordable
The presence object holds a WebSocket per online player, over Cloudflare’s Hibernation API.
A connection waiting on nothing bills no duration. A thousand players sitting in a lobby is not a thousand running objects — it is a thousand parked sockets and an object that wakes when something happens.
Without hibernation, a presence system is a bill that scales with idle users, which is the worst shape a bill can have.
The wiring is the reason it is a capability
A Durable Object is not one thing to declare. It is three, and they have to agree.
A binding in wrangler.jsonc, naming a class.
A class migration tag, also in wrangler.jsonc, in every environment stanza.
An export from your Worker’s entry module — because wrangler resolves the binding’s class name against the module your main names:
export { MultiplayerSession } from "@pithy-sh/multiplayer/src/session/durableObject";Miss the third and deploy is refused: Your Worker depends on the following Durable Objects, which are not exported in your entrypoint file.
pithy add writes all three, across every environment stanza, under a comment saying what the block is. That is tedious to do by hand once and worse to redo per environment — and it is what people mean when they say the wiring is the product.
The export is the one file add writes that is source
So both halves of that write are named, in a dry run and in an apply alike.
It is checked over every composed capability, not only the ones with a missing binding — because a project wired before the CLI wrote that line has the binding already and the export nowhere. That project is exactly who runs pithy upgrade, so it is reported rather than repaired in silence, and pithy doctor fails on it.
Delete a line from the block and the Worker stops deploying. pithy remove takes it back out.
A class migration tag is written once and never revisited
Which is why a Durable Object binding is one of the three kinds declinedBindings refuses to decline.
A decline arriving after an upgrade cannot undo what that upgrade stamped, so the refusal is the honest answer rather than a partial one.
Locally, they are real
pithy dev runs Durable Objects through Miniflare. A game session, a queue and a presence socket all work on your machine with no account, which means the whole path — create a session, join it, play a move — is exercisable before you have provisioned anything.
The cost dial nobody expects
A Durable Object waking on an alarm to do nothing is the shape that surprises people on a bill.
The presence socket is cheap because it hibernates. The queue’s sweep cadence is the dial, because it wakes on a schedule to pair whoever is waiting — and a queue that wakes every second to check an empty lobby costs the same as one that wakes every second to do real work.
What a waiting queue costs has the arithmetic at several queue sizes. It is a short page and it is worth reading before you ship rather than after.
Writing your own
Nothing stops you. Declare the binding, the tag and the export in your own Worker, exactly as a capability does — and pithy worker sync is not involved, because a Durable Object is not a Workflow.
Reach for one when two requests must agree in order. Reach for D1 when they must agree on a fact.