Two people in the same room want to play. A player wants to challenge a friend who is offline. Somebody with nobody to play wants a stranger of roughly their ability, now.
Those are three completely different problems, and most games need all of them plus a fourth. pithy add matchmaking ships all four, and every one of them ends at the same place: a session id that multiplayer can run.
The four
A room code. Short, shareable, short-lived and limited-use. For the person sitting next to you, or a code read out on a stream. The constraints matter — a code that never expires and can be used forever is a code that gets scraped.
A direct invite, by email or screen name. It stays pending until the invitee accepts, which means it works when they are not online. This is the one that turns a game into something people ask their friends to install.
A friend graph, symmetric and formed by mutual accept. Both sides agreed, so there is no follower/blocked asymmetry to reason about, and no one-way relationship that lets a stranger appear in your list.
An open queue, which is the interesting one.
The queue widens as you wait
A Durable Object per game holds the players waiting, bucketed two ways: by region, using Cloudflare’s own edge geolocation, and by skill, read from the rating capability.
The problem with skill-bucketed matchmaking is that a strict band gives good matches to popular games and no matches at all to quiet ones. A player of unusual ability waits forever for a mirror who never logs in.
So each waiting player’s skill band widens the longer they wait, until any available opponent qualifies. Early on you get a close match. Later you get a match. The dial between “good pairing” and “any pairing” is time, which is the one thing a waiting player is already spending.
The presence socket
A second Durable Object holds every online player’s WebSocket and pushes the three things that have to arrive without asking: match found, invite received, friend request.
It runs over the Hibernation API, which matters for the bill. A hibernating connection keeps the socket open but evicts the object from memory, so a player sitting in a menu with nothing happening bills no duration. Presence for an idle user is close to free, which is what makes it viable to keep everyone connected rather than polling.
Why this is a capability and not a snippet
Two Durable Objects means two bindings and two class migration tags, in every environment you deploy.
Class migration tags are the part people get wrong. They are how Cloudflare tracks the identity of a DO class across deploys, and getting them out of step between staging and production produces failures that look nothing like their cause. Doing it by hand twice, across three environments, is six chances to be subtly wrong.
The CLI writes all of it. That wiring is genuinely the reason this is a capability rather than a documentation page with a code sample in it.
Nothing here is a hard dependency
The composition rules are worth knowing, because they let you adopt this in pieces:
| missing | what happens |
|---|---|
auth | every route denies — the right failure |
rating | the queue buckets by region alone |
multiplayer | session minting is off; the rest still works |
So you can ship room codes and a friend graph before you have written a game, and add skill bucketing when you have ratings worth bucketing on. Compose auth at minimum.
What it does not do
It does not do lobbies, chat or spectating. Finding an opponent and hanging around talking to them are different features.
It does not guarantee a match. A queue with one player in it pairs nobody, however wide the band gets, and no amount of matchmaking logic invents an opponent for an empty game.
And it does not decide what a fair match is. It buckets on the skill number you give it, from an algorithm you chose. If the ratings are wrong, the matches are wrong — matchmaking is only as good as its input.