In 2004, a browser could hold an inhabited 3D world on the far side of a telephone line. RuneScape did not make that possible by finding one miraculous compression algorithm. It made thousands of small agreements between the client and server, then stopped sending facts that both sides already knew.
The result is easiest to understand through one ordinary action: click the tile immediately north of your character. In the protocol examined by James Monger from a decompiled 2004 client, that request costs seven bytes. After the server accepts the movement, a nearby player can learn about it through a scene update of roughly nine bytes. Sixteen application bytes complete the useful round trip.
Those figures are not the whole cost of TCP, nor are they a universal measurement for every RuneScape revision. They are a clean window into the design. The client finds paths locally. The server validates them against its own collision map. Both sides retain a synchronized model of the visible world. High-frequency state is encoded in individual bits, while larger, rarer detail records stay byte-aligned so the server can cache and copy them.
This is more than nostalgia for heroic byte shaving. It is a practical example of choosing a protocol around its actual value ranges, repetition patterns, latency model, and compute budget.
Start with the real constraints
A 56k modem’s advertised rate was measured in kilobits, not kilobytes. After line conditions and protocol overhead, treating the downstream budget as roughly 5 KB/s is a useful planning number. Upstream was tighter. A crowded scene, chat, combat, inventory changes, map loading, and control traffic all had to share it.
RuneScape 2 also ran as a Java applet. The sandbox was valuable to users because downloaded code could not freely inspect the machine. It constrained the network design at the same time. Oracle’s archived applet security documentation describes sandboxed applets as able to connect back to their origin while being blocked from arbitrary third-party hosts and native libraries. RuneScape’s game traffic therefore used a regular TCP connection rather than a custom native UDP stack.
The third constraint was deliberate: the game world advanced in cycles of roughly 600 milliseconds. A command that arrived just before the next cycle might run almost immediately. One arriving just after could wait nearly a full tick. Once that rhythm was part of the game, reducing a client flush from tens of milliseconds to a handful did not transform the response time. The worthwhile optimization target was bandwidth.
That distinction matters. A team can spend months optimizing a layer that is hidden under a larger scheduling quantum. RuneScape’s protocol was shaped around the bottleneck that remained visible: the number of bytes multiplied across every player and every tick.
A click begins as local computation
The browser already had a collision map for the loaded area. When the player clicked a destination, the client ran a breadth-first search locally and constructed a route. It did not upload a continuous stream of joystick positions, and it did not send every traversed tile.
The walk request carried:
- one opcode byte identifying the operation;
- one byte giving the variable body length;
- two 16-bit values for the first waypoint’s absolute coordinates;
- two signed bytes for each later waypoint, expressed as an offset from the first; and
- one byte for the Ctrl-key movement modifier.
For a single step, there are no additional waypoint deltas. The body is five bytes, and the opcode plus length marker bring the packet to seven.
Longer paths stay compact for two reasons. First, later waypoints are deltas, so each axis needs one byte instead of two. Second, the path contains corners rather than every tile. Ten unobstructed tiles in a straight line still need only the endpoint. The server knows the starting position and owns an equivalent collision map, so it can reconstruct and validate the intervening movement.
This is semantic compression. The protocol is not trying to squeeze an arbitrary coordinate stream after it has been produced. It changes what is produced. A path is represented by the smallest set of decisions needed to recreate it.
The trust boundary is important. Local pathfinding is not permission to dictate a position. It is a compact proposal. A modified client can invent waypoints, but the server can reject movement that conflicts with its own map or rules. Compute can move to the edge without moving authority with it.
The tick turns time into a batch
On each server cycle, the relevant work followed a simple order: read inbound packets, process players and queued actions, construct player updates, then flush outbound packets. NPCs, logins, and other systems added more stages, but the visible state transition retained that read-do-write shape.
The tick does three useful things for the protocol.
First, it gives the server a consistent boundary for resolving simultaneous actions. Second, it batches many small changes into one update per observer instead of emitting a new message for every property mutation. Third, it bounds how often unchanged state must be acknowledged.
The price is equally real. A 600 ms quantum is perceptible. Modern action games often hide delay with prediction and reconciliation; RuneScape made the tick part of combat timing, skilling, and movement. Players learned its rhythm. Once game rules, animations, and content depend on that cadence, reducing it becomes a product redesign rather than a server setting.
The lesson is not that slow ticks are universally good. It is that batching frequency belongs in the product model. If the experience can tolerate a coarser clock, the system gains a natural place to coalesce work. If it cannot, a compact wire format alone will not rescue responsiveness.
Every client keeps a mirror of nearby players
The crucial bandwidth saving appears on the way back. A client does not wait for the server to resend complete player objects. It retains a list of nearby players with their last-known locations, appearances, animations, and chat state. The server’s update packet patches that mirror.
This makes the most common message almost free: nothing changed.
The composite player update begins in bit-access mode. For the local player, a single zero bit says there was no movement and no queued detail change. If the player walked, the record uses an update bit, a two-bit movement type, a three-bit direction, and a one-bit flag indicating whether a detailed block follows. Seven bits describe the step.
The same approach scales across already known neighbors. The packet writes an eight-bit count and then, for each tracked player, one bit indicating whether anything changed. A static crowd of forty therefore costs 48 bits, or six bytes, before any uncommon detail blocks. Eight idle players fit in one byte rather than eight.
This only works because ordering is shared state too. The server and client agree on which tracked player occupies each position in the list. A zero bit does not carry an identifier or schema. Its meaning comes from the client’s existing mirror and the fixed decoding procedure.
Stateful protocols are powerful precisely because they do not repeat context. They are also operationally demanding. Lose the shared baseline, decode with a mismatched revision, or apply updates in the wrong order and the compact message becomes ambiguous. The bandwidth win creates obligations around sequencing, resynchronization, compatibility, and testing.
Relative coordinates shrink the world to the viewport
A player standing somewhere in the RuneScape world needs coordinates large enough to address thousands of tiles. Two 16-bit axes consume 32 bits. A newly visible player, however, cannot be thousands of tiles away. They are near the observer by definition.
The protocol exploits that narrower domain. A new-player record uses an 11-bit player identifier, one detail flag, one teleport flag, and five signed bits for each coordinate offset. Five bits encode values from -16 through +15, enough for the visible neighborhood. Position falls from 32 bits to 10.
The coordinate system is temporarily recentered on the observer. This is a broadly useful technique:
- a map client can send movement relative to the current tile;
- a time-series codec can store deltas from the previous sample;
- an editor can describe an operation relative to the current document revision;
- a replicated simulation can encode entities relative to an area-of-interest origin.
The saving comes from proving that the active domain is smaller than the global one. If an object can only be visible within 15 tiles, spending enough bits to place it anywhere on the planet is waste.
The sentinel at player ID 2047 applies the same reasoning to list termination. One otherwise unused value means “no more players,” so the stream does not need a separate length field for that section. Good binary formats repeatedly look for values the domain cannot produce and assign them protocol meaning.
One packet, two representations
It would be tempting to bit-pack everything after seeing these gains. RuneScape did not. After local movement, tracked neighbors, and new arrivals are decoded, the reader aligns to the next byte and processes detailed updates conventionally.
Those blocks can contain facing direction, animation, chat, hits, graphical effects, forced movement, equipment, colors, combat level, and other appearance data. An appearance record alone can occupy roughly 44 to 80 bytes. A flag byte says which categories are present. A marker in that first byte buys a second flag byte only when one of the rarer high flags is needed.
Why stop packing when the records become larger? Because size alone does not determine whether bit packing pays.
The early fields have enormous repeated slack. “No change” needs one bit but would consume a whole byte in a byte-oriented structure, and it occurs for many players every cycle. Movement direction has eight possibilities, so three bits are sufficient. The saving repeats across the whole visible crowd.
Detailed records have a different shape. A target ID already occupies a complete short. An animation ID and delay are real fields rather than tiny defaults. More importantly, these blocks are uncommon because the earlier bits gate them. There is no crowd-sized multiplier.
Byte alignment also enables reuse. The client caches appearance buffers, and a server can construct a changed appearance once and splice the same bytes into updates for multiple observers. A byte-aligned blob is position independent. If it started at an arbitrary bit offset, every insertion could require shifting and masking based on everything written before it. A few saved wire bits would consume more server CPU across a large fan-out.
The packet therefore optimizes two resources in one message. Its front half spends modest compute to protect dial-up bandwidth. Its back half preserves alignment so expensive detail work can be cached and copied.
The opcode cipher protected structure, not content
After login, packet opcodes passed through ISAAC, Bob Jenkins’s stream-oriented pseudorandom design. The client and server maintained separate streams for each direction, seeded from shared values with a small transformation separating the inbound and outbound sequences.
Only the opcode was enciphered. The body was not confidential. This was an obstacle for packet parsers and unofficial clients, not modern transport security. Without the decoded opcode, a reader did not know which packet schema or length rule to apply, so the remaining bytes were harder to frame. Revisions also shuffled field order, changed endianness, negated bytes, and added constants as anti-cheat friction.
That distinction deserves precision. Obfuscating message boundaries can raise the cost of reverse engineering, but it does not provide the authenticity, integrity, and confidentiality expected from TLS. The 2004Scape Client2 project exists because determined preservationists could source-port the original Java behavior despite those measures. Its maintainers are explicit that their work is a port from recovered client behavior, not leaked Jagex source.
Shared knowledge is the real compression dictionary
The protocol is small because the two programs were designed as halves of one runtime:
- Both have the collision map, so paths can be sent as corners and checked independently.
- Both retain the visible-player list, so a one-bit flag can modify an implied record.
- Both understand the viewport, so local offsets replace global coordinates.
- Both compile the same update-field table, so a bitmask replaces a self-describing schema.
- Both follow the same tick order, so many events can become one deterministic state delta.
General-purpose APIs usually carry more description because their clients evolve independently. Field names, type tags, resource identifiers, version negotiation, and explicit lengths buy flexibility. RuneScape traded much of that flexibility for density.
That trade was rational for a first-party game client shipped alongside its server. It would be dangerous to copy blindly into a public platform. A tightly coupled binary protocol requires coordinated rollout, strong golden tests, careful revision handling, and a recovery path when mirrors disagree. An undocumented client assumption is not free merely because it does not cross the wire.
The preservation story makes this cost visible. Early game versions are incomplete historical artifacts. The 2004Scape server project describes itself as an emulator reconstructed without original server source, while the RuneScape Archive Project collects surviving clients and caches. When a protocol’s schema lives mostly in two matching binaries, losing either side also loses documentation.
Applying the design today
Modern bandwidth is larger, but the multipliers are larger too. Multiplayer simulations, collaborative editors, market feeds, IoT fleets, and mobile applications still pay for fan-out, battery, radio wakeups, serialization, and server assembly. The useful principles survive the modem.
Model the common case first. Measure what happens most frequently, then make that state cheap. If 98 percent of entities do not change during an interval, optimize the unchanged marker before compressing the rare full record.
Narrow the domain before choosing a type. Global identifiers and coordinates may be unnecessary inside a scoped list, viewport, shard, or session. Prove the local bounds and encode those.
Send decisions, not derivable steps. When both ends have deterministic inputs, transmit route corners, operation parameters, or a compact seed rather than the entire derived result. Keep validation authoritative.
Batch on a product boundary. A tick, animation frame, transaction, or document revision can turn scattered mutations into one ordered delta. Choose the cadence from user experience, not only infrastructure convenience.
Switch formats when the economics change. Bit packing is valuable where slack is small and repeated. Byte alignment, fixed records, or a conventional serializer may win where fields are large, rare, cached, or frequently inspected.
Budget server fan-out, not only packet size. A representation that saves two bytes but forces a custom bit shift for every observer may lose at scale. Count encoding work, allocations, cache reuse, and copies alongside wire bytes.
Design resynchronization before launch. Stateful deltas need sequence numbers, snapshots, revision negotiation, or another way to rebuild the baseline. The smaller the incremental message, the more it depends on that baseline being correct.
RuneScape’s elegant result was not “seven bytes” by itself. It was a system in which seven bytes were sufficient because computation, authority, visibility, order, and cached state had already been placed deliberately. The modem forced discipline, but the enduring lesson is architectural: the cheapest fact to transmit is the one both sides can safely derive.
Further reading
- How 2004 RuneScape fit a multiplayer RPG into 56k dial-up, James Monger’s detailed walk through the decompiled client behavior.
- Hacker News discussion, where the article reached 134 points and 83 comments on the September 1, 2026 front page.
- 2004Scape Client2, a TypeScript source port of the May 18, 2004 client.
- 2004Scape Server, an independently written emulator focused on reproducing the May 2004 experience.
- What applets can and cannot do, Oracle’s archived guide to the Java applet sandbox.
- ISAAC, Bob Jenkins’s original description and implementation notes.

Top comments (0)