John Aleman
Back to Projects
t: "post"Cloudflare Workers & Durable ObjectsAugust 2026

The Room That Sleeps

How I built a real-time game that spends most of its life fast asleep — and somehow still remembers everything the second it wakes up.

Spot the Difference is a small multiplayer game on this site: one player hosts a round, everyone else joins with a code, and the group races to spot the one flower that's different from all the rest. The game itself is the easy part. The hard part was the computer running it — a program without a server sitting around waiting, one that can fall completely asleep for an hour and then wake up remembering exactly where it left off.

A game like this needs a live, two-way connection — a WebSocket, the same kind of connection behind a live chat app or a multiplayer score screen — so players see updates the instant they happen instead of refreshing the page. That part's pretty normal. What's not normal is what's on the other end of it: not a server that just stays on and listens, but a program that gets woken up out of a dead sleep every single time something happens, with no guarantee that its memory will still be there the next time it wakes up.

t: "architecture"

One object, one room

This website — the one you're reading right now — never actually touches the game while it's happening. It shows the game page, and then, once at the very end of a round, it gets a single message with the results. Everything in between happens somewhere else entirely: a separate little project called workers/spot-the-difference (not to be confused with /projects/spot-the-difference, the page you're playing on — same name, two different things), with its own settings and its own way of getting deployed to the internet. It lives on Cloudflare, not wherever this site normally lives, and the two only share a folder so they can't accidentally drift into speaking different languages to each other.

fig. 1 — the Worker only brokers the socket; the Durable Object is the room, and this app only ever hears from it after the round is over.

When you open the game, your browser opens a WebSocket to an address on Cloudflare that's always ready to accept a connection. That address's only job is to hand your connection off to something called a Durable Object. There's one Durable Object per room code, so each room effectively has its own object — think of it as a tiny robot assigned to one room. That Durable Object keeps track of the timer, the score, who's playing — everything, held in memory while the object is awake. When the round ends, it sends one message back to this website to save the results for good. The live game and the saved results live in two different places on purpose: a Durable Object's in-memory state is fast but forgets easily, while the database is durable storage — where a result becomes permanent, like writing it in pen instead of pencil.

Why a Durable Object instead of a normal always-on server? This kind of program doesn't leave anything running in the background between requests. When the Durable Object is awake, it can keep state in memory, but that state can't be relied on once the object goes to sleep. Every Durable Object also gets its own permanent address, like a name tag. So instead of searching a big list for “room ABC123,” the computer just wakes up the one Durable Object literally named ABC123. The address is the lookup. For this game, that tradeoff made sense: each room is small, rooms are mostly idle, and there's no reason to pay for compute that spends most of its time waiting.

t: "boundaries"

Rules that can't see a socket

The game's code is split into three areas, kept apart on purpose — nobody's forced to follow the rule, everyone just does: one folder knows about WebSocket connections and durable storage. A second folder knows about flowers, scoring, and the rules of a round, but has never heard of a network connection. A third folder knows only what a message looks like, and nothing about either of the other two.

fig. 2 — solid arrows are allowed imports; the dashed one is the direction protocol/ must never take, back up into rules or transport.

Each game mode is written as a pure function: feed it the current game state and an event — whatever just happened — and it hands back the new state plus a list of things to do next. The Durable Object doesn't actually understand the rules of the game at all — it just carries out whatever list of instructions that function gives it, like a mail carrier delivering letters without reading them. Because the rules live completely apart from the networking code, the entire rulebook — was that click legal, who scored, when a round ends — can be tested without ever opening a real connection. Adding a whole new game mode just means writing a new folder full of rules. Nothing about how the game actually connects to players has to change.

t: "protocol"

Hostile until proven otherwise

Every WebSocket message that arrives could, in theory, have been typed by anyone — including someone actively trying to break things. One file, codec.ts, has exactly one job: turn that untrusted mess into something the rest of the program is allowed to trust.

There's no fancy validation library doing this, on purpose: the Worker ships with zero extra runtime dependencies, the list of possible messages is small, and a hand-written check that can't ever crash is easier to trust than a big library doing something magic behind the scenes. The checker looks at what kind of message just arrived and inspects every single piece of it by hand before letting it through. It either hands back a fully-validated message, or a clear “this one failed” — never something half-checked.

fig. 3 — the only two ways out of parse(). Nothing downstream ever sees an unchecked field.

Even the “hi, I'm here” message deliberately leaves some blanks — a name, an email, a reconnect code — because whether those are actually required depends on things the checker has no way of knowing yet, like whether you're a brand-new player or someone rejoining after a dropped connection. That decision gets made a step later, once the game actually knows what's going on.

The most interesting decisions happen in the two functions that clean up names and emails before they're stored. Since names show up on a public weekly leaderboard, the name-cleaner strips out invisible characters and normalizes lookalike character combinations — without that, hidden characters could make one name display like another while the underlying strings stayed different, letting two visually identical names sneak onto the leaderboard as separate entries. The email-cleaner is simpler: lowercase everything, trim the spaces, and stay pretty loose — nobody's email is being verified here, the only goal is stopping a typo from silently creating a ghost duplicate of a real player.

One last small trick: if there's only one message to send, it goes out by itself instead of wrapped in a list, because sending fewer, bigger batches reduces the number of messages the game has to send — and a lot of moments in the game (like revealing the answer) naturally fire off several messages at once anyway. Whatever reads messages back in on the other end handles both shapes, so the game always uses the fewer-message option.

t: "hibernation"

The object that isn't there until it is

Everything above assumes the Durable Object is awake. Most of the time, it isn't.

fig. 4 — the object wakes for a message the same way it wakes for an alarm; rehydration and reconciliation don't care which one it was.

Cloudflare hibernates an idle Durable Object to free up memory, and getting that right meant unlearning a few habits that work fine for normal programs. Accepting a new connection has to be done one very specific way — get it wrong, and the Durable Object silently loses the ability to ever sleep again, for its entire lifetime. Every time it wakes back up, its startup code runs from scratch, which means nothing in memory can be relied on to still be there. Instead, it rehydrates — rebuilding its game state from durable storage and reconnecting to its players — every single time, as if it had never run before, because as far as it's concerned, it hasn't.

t: "alarms"

When “later” doesn't survive sleep

Two very ordinary tools — ways of saying “do this in a few seconds” — don't survive sleep at all: a pending one either keeps the object awake against its will, or gets silently thrown away the moment it does fall asleep. So anything that absolutely has to happen later uses a different mechanism instead, called an alarm. But alarms come with their own fine print: they might fire more than once, they might fire late, and in the worst case, one might silently never fire at all. To guard against that, firing has to be idempotent — an operation where firing twice produces the same result as firing once. In practice, the game uses a duplicate-proof token, so a repeat firing does nothing extra.

There are two more safety nets: a backup check that runs every time the object wakes up for any reason at all, and a lazy double-check that runs whenever a player sends any message whatsoever — so even total silence eventually gets caught.

Two smaller gotchas round it out. Asking “what time is it right now” doesn't actually update unless the program is in the middle of talking to the network — so checking it twice in a row can return the exact same instant even an hour apart. You have to grab it once and reuse it. And there's a setting that answers simple “are you still there?” pings automatically, without fully waking the object up. Skip it, and every single ping from a player's browser fully wakes the whole thing just to say “yep” — and it never actually gets to rest.

Underneath everything, the actual game is simple: spot the one image that doesn't match the rest before the timer runs out.

It's live at /projects/spot-the-difference if you want to see what all that work buys you: a game that reconnects players cleanly, survives someone closing their laptop mid-round, and runs without a server sitting there, running and waiting for something to happen.