I build TagKing.io on my own: a two player game of tag that runs in a browser tab. One player is "it", touching the other passes it on, and whoever is "it" when thirty seconds run out loses. There is a ranked ladder, so it has to work online between strangers.
This week I finally got a friend on a call to play me, and it took far longer than it should have. Three things broke, and each one taught me something I wish I had known before writing a line of netcode.
The short version: in a small multiplayer game, the failures are almost never where you are looking.
1. Pressing Play at the same moment was cooked (never meeting)
My matchmaking was simple. When you press Play Online, the server looks for somebody already waiting. If there is one, you are paired. If not, you open a waiting slot, and after a few seconds a bot fills in so nobody stares at a spinner.
My friend and I pressed Play together. Each of our requests arrived before the other's waiting slot existed, so both of us opened empty slots, nothing ever looked again, and a few seconds later we each got a bot.
The fix was to stop treating "look for an opponent" as something that happens once. Every waiting player already polls the server about once a second, so each of those check-ins now looks for another waiter too:
The id < ? is the important part. If both players check in at the same instant, only the one with the larger id is allowed to claim the other, so they cannot claim each other at once. I also pushed the bot back from two to five seconds to six to nine, because a bot that arrives before a second check-in can happen is a bot that always wins the race.
2. The match froze, then fast-forwarded
Once we were connected, the match would sometimes stop dead, then jump forward as if the other player had teleported.
The game sends positions over a WebRTC data channel set to unordered with no retransmits: a late position is worthless, so there is no point resending it. But when the connection could not keep up, the browser was still buffering every frame I handed it and delivering the backlog all at once.
The fix is one line. Before sending a frame, check how much is already waiting:
Skipping a frame while the buffer is backed up means the freshest position goes out as soon as there is room, instead of a queue of stale ones.
3. My own analytics took the site down
The game runs on a Cloudflare Worker with a SQLite database on the free tier. Reads count against a daily allowance that is shared by everything: players signing in, the leaderboard, and me.
Twice now I have run ad-hoc queries while investigating a problem, once a self-join on the visits table that read about eighteen thousand rows to count fourteen devices, and pushed the day's usage over the limit. When that happens, sign-in fails for real players until the reset.
The lesson I keep relearning: on a shared budget, your debugging is a user. I now only run single-table counts over a bounded time window, and I record how many rows each query read.
What I would tell myself before starting
Test the cases where two things happen at the same time. That is where matchmaking breaks.
For real time state, dropping data is often correct. Queueing it is the bug.
If your tools share a budget with your players, budget your tools.
If you want to see how the tag feels, the game is at tagking.io. It runs on a laptop keyboard, and you can play a friend in a lobby or someone on the same keyboard. I would genuinely like to hear how the movement feels to someone who did not build it.

Top comments (0)