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:receiveType | Ratio on odds | Client work | dict frames |
|---|---|---|---|
"zstd" | ~5–6× | One line: zstd.decompress(frame) → JSON | none |
"zstd-dict" | ~7–9× | Cache the dictionaries the server pushes, decode each frame by its embedded dictId | sent at connect |
"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
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 afterlogin_ok (and before any data frame), for each subscribed channel that has a trained
dictionary the server pushes one control frame:
- Base64-decode
datato the raw dictionary bytes. - 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.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 embeddeddictId). - 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
Requirespip install zstandard.
- zstd (dictless)
- zstd-dict (dictionaries)
See also
- Auth & Filters — full
loginfield reference - Resume & Replay — reconnecting and recovering missed data
- Troubleshooting — zstd decoding issues