Over the weekend, my friend’s son asked me to help him build a game. It was the second time he had asked, so this time I couldn’t say no, and I decided to build it on MOQtail. Why not?
It started with a very simple idea: a multiplayer plane shooter that runs in the browser and also works on a phone or tablet, because kids mostly play on tablets. The first version took half an hour. We were on the same Wi-Fi network, and my laptop ran the MOQtail relay. I played in a browser on my laptop while he played in Chrome on his Android tablet. It was fun.
After that first version, I kept adding features. A few (maybe more) hours of work and many Claude tokens later, DOGtail was born. It’s a multiplayer plane shooter that runs in the browser. Everything you see, every plane, bullet, kill, emoji reaction and pilot picture, travels through the MOQtail relay. There’s no game server and no WebSocket fallback. You can play it at dogtail.moqtail.dev.
Note: DOGtail works in Firefox too, not only in Chrome and Safari. I’ve tested it there.

A match against two hard aces. Your jet sits in the middle, with the scoreboard at top right and the minimap at the bottom.
Why a game?
Our MOQtail tests and demos had all been about media streaming: one publisher, many subscribers, a steady bitrate and big objects.
A game is different in many ways:
- Everyone publishes. Each player is a publisher and a subscriber at once. A room of ten has ten tracks going in and ninety coming out.
- Many tiny, frequent messages. A plane’s position is a few dozen bytes, sent ten times a second. A late position is useless, because the next one is already on its way.
- Players come and go all the time. They join, leave, close the laptop lid, switch or close tabs, and lose Wi-Fi on a train.
- Timing is visible. In a video stream, a buffer or pacing can hide 200 ms of extra latency. In a game like this, it’s a bullet that should have hit and didn’t, and the player notices straight away.
- Every browser matters. People open the link on whatever they have: Chrome on a Mac, Safari on an iPhone, an iPad.

On a phone: touch buttons, a zoomed camera and arrows pointing to players off screen.
All of this makes a game a challenging test, and a different angle on the MOQtail relay.
How it was built
The first version was just a canvas, some triangles and one MOQ track per room. Dozens of commits later (squashed for a clean start), it’s a standalone repository, moqtail/demo-dogtail, with:
- jets with emoji pilots, or your own photo (20×20 pixels, small enough to fit in a single datagram);
- a scoreboard, a kill feed, quick reactions, power-ups and a minimap;
- zoom on phones, tablets and desktops, plus fullscreen and Home Screen modes;
- a network stats panel built from the client library’s own events;
- load-test bots that fill a room from a headless browser;
- aces: computer pilots trained with reinforcement learning, which fill quiet rooms at an easy, moderate or hard level.

The join screen: pick a name, a color, an emoji pilot or your own photo, and a room.
I kept the design simple on purpose:
- Each player publishes one track. Its namespace is the room and the player id.
- Everyone subscribes to the room’s namespace. New players show up on their own.
- Everything goes as datagrams. I started with subgroup streams but switched to datagrams for the real-time updates, and the game felt better. Safari gave me another reason, as you’ll see below.
- No server-side game logic. Each player decides whether they were hit and announces it. That’s easy to cheat, but it’s right for a demo, because the relay should only move bytes.
The game is a few thousand lines of TypeScript on top of the published moqtail npm package. So it tested not only the MOQtail relay but also our MOQtail TypeScript library.
What it revealed about MOQtail
This is the real reason for this post. During development and testing, the game turned up problems that our unit, interop and conformance tests had missed.
1. Idle players disappeared. A player who stopped moving would vanish for everyone else. The cause was in the publisher: once a subscriber had stopped a stream (STOP_SENDING), publishing on that subgroup failed and the track stayed dead. It only happens when an object arrives after a stop, which a steady video stream never does. I fixed it in MOQtail in PR #464.
2. Safari stopped receiving after a minute. On Safari, other players froze after a few dozen updates. Safari stops granting credit for streams the server opens after about a hundred of them (WebKit bug 319818). A video player opens a handful of streams; a game that opens a new stream for every update hits the limit almost at once. I moved all game traffic to datagrams, which suit this kind of data anyway: a lost position doesn’t need resending.
3. Safari couldn’t send datagrams either. Safari’s WebTransport has no datagrams.writable, only createWritable(), and our client library assumed the first. The game shipped with a small workaround while I put the real fix into the library in PR #465. Both fixes are in moqtail 0.16.0, and the game now uses that release with no workarounds.
4. Small details. For example, the relay doesn’t send a session’s own tracks back to it, so a tab hosting several aces has to deliver their messages to itself locally. That’s the correct behavior, but building something like this makes you notice it.
Pushing the limits
Once it worked, I tried to break it with load-test bots. Each bot is a full client in a headless browser tab: it publishes its own track and moves and shoots like a person. The bots let me fill a room to its cap and watch the relay’s fan-out, while the network stats panel shows each client’s round-trip time and loss as it happens.

The stats panel (S key): relay round-trip time, what this tab sends, and delay, jitter and loss for every other player.
Computer pilots
While testing, I opened browsers on several of my devices so there would be some stray planes to shoot. That wasn’t fun. So I trained a reinforcement learning agent with PPO (Proximal Policy Optimization, an actor-critic method), using self-play in a Python copy of the game’s physics. I checked the copy frame by frame against the TypeScript game, so what the agent learns holds in the real game. The trained pilots are the aces.
Training took about 25 minutes on my laptop. The result beats the hand-written, rule-based pilot by about three kills to one. The network runs in the browser in plain TypeScript, so there’s no ML runtime to download.
The interesting MOQ part is how aces join. One player’s tab hosts them, and each ace publishes its own track on that player’s connection, exactly like a person. To the relay and to the other players, an ace is just another player. When the host leaves, the next player’s tab takes over.

A hard ace closing in. The kill feed at top left shows who shot whom.
At first there were no levels, and the aces were very hard to beat. So I added easy, moderate and hard. Each level weakens the same network while it plays, mostly by slowing its reactions, with no retraining.
Aces are a new use case too: many publishers behind one connection, competing for the same bandwidth.
Why demos matter
On the MOQtail team, we love demos, because for a protocol implementation a good demo is part of the work:
- It’s an integration test you can’t fake. Every layer runs together, on networks and devices you don’t control.
- It finds latent bugs. Each bug above was in code that had passed its tests. They needed an unusual mix of timing, traffic shape and browser to show up.
- It tests the API, not just the wire. Writing an app on your own library shows what’s missing or awkward. The game used only the public npm package, so every problem it found would also hit every user of that package.
- It explores new use cases. MOQ isn’t only for video and audio. Gaming, chat, telemetry and collaborative apps all need low-latency publish/subscribe, and you only truly understand their needs by building one.
- People actually use it. You send a link, and a colleague on an iPhone finds the Safari bug in ten seconds. That’s faster than any test matrix. At the end of the day, technology is for people.
Try it
- Play: dogtail.moqtail.dev
- Code: github.com/moqtail/demo-dogtail. The README shows how to run it against a local relay.
- MOQtail: github.com/moqtail/moqtail, and the TypeScript library on npm
If you find a bug, we’d like to hear about it. That’s what the game is for.