Backend6 min read
Binary WebSocket over JSON
The new game server speaks packed binary frames instead of JSON. Fewer bytes per tick is the advertised win. The real one is that the format finally has a contract.
What a tick actually carries
A farming game does not send prose. It sends the same handful of numbers many times a second: who moved, what grew, what is now owned by whom. In JSON, most of what crosses the wire is not those numbers at all. It is their names, re-sent on every frame, quoted, in case the reader forgot them since last time.
That is the cheap argument for binary, and it is true. It is also the least interesting one.
Why binary, and why not sooner
The old backend was 27,378 lines of Python and FastAPI, and it was replaced piece by piece rather than in one go, which is the only way to move a live game. The protocol was one of those pieces, and it was not the first, because changing the wire format before the server behind it is stable means debugging two mysteries at once.
So the order was: make the new server correct with the old-shaped messages, then change the shape. Boring, and about twice as slow as the version of this story where I am clever.
A contract instead of a convention
Here is the part I would argue for even at the same byte count. A JSON tick is a convention: every client is free to read an absent field as zero, a number as a string, a new key as noise. Nothing breaks loudly, so nothing gets fixed early.
writeUInt16(view, cursor, frame.entityId);
writeUInt8(view, cursor + 2, frame.state);
// the field order is the schema: a mismatch throws here, not three screens laterA packed frame has to be agreed on before it can be read at all. The format stops being folklore and becomes something two programs can be wrong about out loud.
The part that surprised me
I expected the win to be bandwidth. The win was debugging. When a frame is a fixed shape, a bad frame is obvious at the boundary, with the tick that produced it still in hand, instead of arriving three screens later as a plant that quietly refuses to grow.
The cost is real: you cannot read the wire with your eyes, so you owe yourself a decoder in the tooling from day one, and a version marker from the first frame you ever send.
The order I would keep
Change the protocol after the server, not before. Write the decoder for humans on the same day as the encoder for machines. And treat the schema as the product: the bytes are just how it travels.