Skip to main content
Beta β€” rolling out per region. zstd is being enabled gradually. If a gateway has it disabled, a receiveType: "zstd" or "zstd-dict" connection is gracefully downgraded to JSON β€” the server tells you the negotiated mode in login_ok.receiveType. Always trust that echo to choose your decoder; never assume the mode you requested.

Two modes

Both compress every data frame with zstd; control frames stay JSON text. Pick based on how much client work you want to do: Start with "zstd". It needs no dictionary handling at all β€” decompress and parse, done. Move to "zstd-dict" when you want the extra ~30–40% and can keep a small in-memory dictionary store.
There is intentionally no compressed-MessagePack mode β€” compressing JSON beats compressing MessagePack at every level.

Mode 1 β€” zstd (dictless)

The soft-adoption path. Every data frame is a standalone, dictless zstd frame (dictId = 0); the server sends no dict control frames. Your client only decompresses and parses.

Login

Decode

That’s the whole protocol for this mode.

Mode 2 β€” zstd-dict (trained dictionaries)

The maximum-ratio path. odds, fixtures, and bookmakers have trained ~32 KB dictionaries that push the ratio to ~7–9Γ—. The server delivers them as control frames at connect; each data frame embeds the dictId it was compressed with, so decoding is self-describing and never branches on channel.

Login

Dictionary delivery (server β†’ client)

Right after login_ok (and before any data frame), for each subscribed channel that has a trained dictionary the server pushes one control frame:
  1. Base64-decode data to the raw dictionary bytes.
  2. Build a reusable zstd decoder from it, stored keyed by dictId.
Decoding is driven by the dictId embedded in each data frame, not by channel. The channel/dictVersion fields on the dict frame are informational.
The dictionaries are small (~32 KB) and re-sent on every connection β€” there is no client-side version cache to maintain and no dicts field to send at login. Channels without a trained dictionary (e.g. scores, clocks) send no dict frame; their data frames carry dictId = 0 and decode dictless.

Decode


Frame rules (both modes)

  • Every data frame is a WebSocket Binary frame containing one standalone zstd frame (magic 28 B5 2F FD, with an embedded dictId).
  • Control frames (login_ok, dict, error, snapshot_required, resume_complete) stay JSON text frames, even on a zstd connection. The rule is fixed:
Text frame β†’ control (JSON). Binary frame β†’ data (zstd).
login_ok.receiveType is the negotiated mode ("zstd", "zstd-dict", or β€” if the gateway has zstd disabled β€” "json"). If it comes back "json", decode plain JSON text frames; do not try to decompress.

🐍 Python examples

Requires pip install zstandard.

See also