Realtime Applications
Published
Realtime products — collaborative editors, trading screens, multiplayer games, live dashboards — tend to have an unusual property: the protocol is the product. Years of work go into the message set, the reconciliation rules and the reconnect behaviour, and all of it is implemented in a client that runs on hardware you do not own. The question that follows is reasonable, and this article answers it in two halves: what protection genuinely does for a realtime client, and which parts of the problem it was never going to touch.
The frames are visible, and that is not a bug
Begin with the part that ends one line of hope quickly. Your server accepts messages in a particular shape, so the browser must produce that shape. The frames it produces are listed, in both directions, in the browser’s own network panel — message type, payload, timing, all of it — and anyone running a proxy on a device they control sees the same stream. This happens without anyone opening your JavaScript.
So an observer who spends an afternoon with your application learns the message vocabulary: which types exist, what fields they carry, what the server sends back, and the rough sequencing of a session. Protection does not change this by a byte, because it operates on your code and the frames are your protocol.
A binary encoding does not rescue it either. Protocol Buffers, MessagePack, or a hand-rolled layout make the payload less pleasant to read, and that is a real speed bump. But the decoder is in the client you shipped, and the field layout is recoverable from it. Binary formats are excellent choices for bandwidth and parse cost, which are good reasons; treat improved unreadability as a side effect rather than a control you can lean on.
The same reasoning covers custom framing, opcode numbers instead of names, and shortened field keys. Each raises effort a little. None of them change what a patient reader can establish, and none of them affect what an impatient one can do, which is the next section.
The failure that actually costs people money
Realtime applications have a characteristic security bug, and it is not that someone read the protocol. It is that the server authorized the connection and then trusted the traffic.
The pattern is easy to fall into. The handshake carries a session cookie or a token, the server checks it, the socket is established, and from that point the message loop is written as if the peer were your own interface. It is not. It is a channel over which the client can send anything, at any rate, in any order — including messages your UI has no way to produce, messages for objects the user cannot see, and the same message four thousand times in a second.
Every frame therefore needs the same treatment an HTTP request would get: validate the shape, reject anything unexpected, and check the sender’s permission for that specific action on that specific object, resolved from a server-held session rather than from an identifier in the payload. A message carrying room or tenant or userId is a claim, not a fact.
Rate and cost limits belong on the socket too. HTTP endpoints usually inherit a limiter from the gateway; a long-lived socket frequently bypasses all of that, which makes it the cheapest denial-of-service surface in many applications and an efficient one for scraping a dataset message by message.
Notice that none of these are affected by how the client is built. They are the controls that decide what an attacker can do once they understand the protocol, which is the question that matters, given that understanding it was never preventable.
What protection is genuinely worth here
Now the other half, because there is a real answer and it is more interesting than the protocol.
Watching traffic tells an observer what was sent. It does not tell them why, and in a mature realtime client the why is where the engineering lives. Consider what an unprotected bundle hands over: the reconnect and backoff policy, including the jitter that stops a reconnect storm taking out your own server after an outage. The resynchronisation strategy after a gap — whether the client asks for a delta, a snapshot, or a versioned replay, and how it decides. The optimistic update rules: which actions render locally before the server confirms, and how a rejection is rolled back without the interface flickering. Conflict resolution, which in a collaborative editor is the crown jewel. The subscription thresholds, degradation ladders and back-pressure handling that keep a busy screen responsive.
An observer can infer fragments of this from behaviour, slowly and unreliably. They can read all of it from a readable bundle in an afternoon. That asymmetry is exactly what obfuscation is for, and it is a much better description of the value than any claim about hiding messages.
There is a tampering dimension too. A realtime client typically contains client-side prediction and validation that exist for responsiveness rather than security — but a modified client that skips them, sends malformed states, or fires actions faster than the interface allows is a real problem for games and trading screens in particular. Making in-place modification harder, and wiring tamper signals into your monitoring, is what runtime defense does. The important caveat is that this raises cost rather than establishing a guarantee: the server still has to reject what a modified client sends, because it is the only participant you control.
Two configuration details specific to sockets
The runtime guard watches WebSocket by name. The anti-monkey-patching default watch list includes the global WebSocket constructor, next to fetch, XMLHttpRequest, EventTarget.prototype.addEventListener and navigator.sendBeacon. The guard compares each watched function against a pristine copy and re-checks on a five-second interval, so anything that replaces the constructor trips it. That is the intended behaviour when the replacement is hostile instrumentation — and it is also what a debugging proxy, a session-replay tool, or a developer’s own logging wrapper does. If you deliberately wrap WebSocket, exclude that path with AntiMonkeyPatchingExcludeGlobals rather than discovering the interaction during an incident. The same consideration applies to the analytics-shaped wrappers discussed in the tag manager article.
String encryption and hot message loops do not mix. Message-type constants are a tempting target for Encrypt Strings, and it is the wrong place for it. Values are reconstructed on every read rather than parsed once, which is invisible in ordinary UI code and very visible in a dispatcher handling hundreds of messages a second. You would also be paying that cost to hide constants that appear in every frame on the wire anyway. Spend the option on strings whose content is the asset — internal endpoints, licence states, rule names — and leave the protocol vocabulary alone. The same guidance holds for virtualized functions: bytecode is right for a licence check that runs once and wrong for a message loop.
Handshake credentials, briefly
One recurring question deserves its own answer: can the token for the socket live in the client?
Not if it is long-lived. Everything in the bundle is readable by everyone who downloads it, which protection raises the cost of but does not change in kind — the argument is set out in full in you cannot hide an API key in JavaScript. The pattern that works is a short-lived, single-use ticket: authenticate the user through your normal flow, have the server mint a ticket scoped to that user and valid for a few seconds, and let the client present it at the handshake. A copied ticket expires before it is useful, and the durable credential never enters the bundle.
While you are there, check where the ticket ends up. Query strings are logged by proxies and retained in places you do not control, so prefer a mechanism that does not put the ticket in the URL, and confirm the connection is wss:// so the frames are not readable in transit by anyone other than the endpoints.
The short version
Your message format is visible in the browser’s network panel and to any proxy the user runs, and no build step changes that; binary encodings and opcode numbers slow a reader down without concealing anything. Put the effort where it decides outcomes: authorize every message rather than the connection, treat every frame as untrusted input, resolve identity from the server session rather than the payload, and rate-limit the socket the way you rate-limit HTTP. Then protect the client for what it uniquely holds — reconnect, reconciliation, prediction and conflict logic that never appears on the wire. Exclude WebSocket from the anti-monkey-patching list if you wrap it on purpose, keep string encryption out of the message dispatcher, and hand the socket a short-lived ticket instead of a durable token.
Frequently asked questions
Does obfuscation hide my WebSocket message format?
No. The browser has to send well-formed frames for your server to accept them, so the wire format is whatever your server expects regardless of how the client code looks. Every frame in both directions is listed in the DevTools network panel with its payload, and a proxy on a device the user controls sees the same. Obfuscation changes the code that builds the message, never the message.
Does using a binary format such as protobuf or MessagePack make a realtime protocol secret?
It makes it less convenient to read, which is not the same thing. A binary encoding is a compression and typing decision, and the schema needed to decode it is in the client you shipped. Anyone willing to spend an hour recovers the field layout from the decoder in your own bundle. Choose binary formats for bandwidth and parse cost, and assume anything you send is legible to a determined reader.
Where should authorization happen in a realtime application?
On every message, not once at the handshake. Authorizing the connection and then trusting its traffic is the most common realtime security mistake, because a socket is a long-lived channel over which the client can send anything at any time, including messages your interface would never produce. Treat each frame as untrusted input, validate its shape, and check the sender's permission for that specific action against a server-held session rather than an identifier in the payload.
What does obfuscation genuinely protect in a realtime client?
The parts an observer cannot infer from watching traffic. The reconnect and backoff strategy, how the client reconciles state after a gap, which local predictions are applied before the server confirms them, how conflicts are resolved, and the thresholds that decide when to degrade or drop a subscription. That logic is often the most refined code in a realtime product, it is invisible on the wire, and it is fully readable in an unprotected bundle.
Can I put an authentication token for the socket in my JavaScript?
Not a long-lived one. Anything embedded in the bundle is readable by whoever downloads it, which is everyone. The workable pattern is to authenticate the user by the normal route, have the server mint a short-lived single-use ticket for the socket, and hand that to the client just before it connects. The ticket then expires quickly enough that a copied one is of little use, and the bundle contains no durable credential.
Do the runtime guards interfere with WebSocket connections?
They watch it, which is worth knowing before enabling them. The global WebSocket constructor is on the default anti-monkey-patching watch list, alongside fetch, XMLHttpRequest, addEventListener and sendBeacon, so code that replaces the constructor to instrument or log traffic looks exactly like tampering to the guard. Debugging proxies, some analytics wrappers and browser extensions do precisely that. The documented escape hatch is AntiMonkeyPatchingExcludeGlobals, which removes a specific path from the list.
Related reading