> ## Documentation Index
> Fetch the complete documentation index at: https://docs.struct.to/llms.txt
> Use this file to discover all available pages before exploring further.

# Compression

> Opt into zstd compression on the Rooms websocket for high-throughput sockets.

High-throughput sockets can opt into **zstd** compression. It's negotiated once at the WebSocket upgrade and is **connection-scoped**: it applies to every room subscription on that connection, and can't be changed without reconnecting.

When enabled, room envelopes arrive as **binary frames of zstd-compressed JSON** rather than text frames. Everything else about the protocol is unchanged, only the wire format of inbound messages differs.

## Enabling compression

Request compression at the handshake in one of two ways:

| Method | How                      | When to use                                                                    |
| ------ | ------------------------ | ------------------------------------------------------------------------------ |
| Header | `X-Ws-Compression: zstd` | Preferred. Any client that can set upgrade headers.                            |
| Query  | `?compression=zstd`      | Fallback for browsers, which can't set headers on the `WebSocket` constructor. |

```
wss://api.struct.to/ws?api-key=YOUR_API_KEY&compression=zstd
```

Omit both the header and the query parameter for the default uncompressed text frames. `zstd` is the only supported value; an unsupported value fails the upgrade with `400`. Subscribe-message compression is not used, only the handshake matters.

<Note>
  The header is preferred because it keeps the compression choice out of the URL and logs. Use the query parameter only when your client can't set upgrade headers, as is the case for the browser `WebSocket` constructor. If you set both, they must agree.
</Note>

## Decoding frames

A compressed connection delivers each room envelope as a binary frame. Decompress the bytes with zstd, then `JSON.parse` the resulting UTF-8 string exactly as you would an uncompressed text frame.

<CodeGroup>
  ```typescript Node (ws + fzstd) theme={null}
  import WebSocket from "ws";
  import { decompress } from "fzstd";

  const ws = new WebSocket("wss://api.struct.to/ws?api-key=YOUR_API_KEY", {
    headers: { "X-Ws-Compression": "zstd" },
  });

  const decoder = new TextDecoder();
  ws.on("message", (data: Buffer, isBinary: boolean) => {
    const raw = isBinary ? decoder.decode(decompress(new Uint8Array(data))) : data.toString();
    const msg = JSON.parse(raw);
    console.log(msg.type, msg.message);
  });
  ```

  ```javascript Browser (query fallback + fzstd) theme={null}
  import { decompress } from "fzstd";

  const ws = new WebSocket("wss://api.struct.to/ws?api-key=YOUR_API_KEY&compression=zstd");
  ws.binaryType = "arraybuffer";

  const decoder = new TextDecoder();
  ws.onmessage = (event) => {
    const raw =
      event.data instanceof ArrayBuffer
        ? decoder.decode(decompress(new Uint8Array(event.data)))
        : event.data;
    const msg = JSON.parse(raw);
    console.log(msg.type, msg.message);
  };
  ```

  ```python Python (websockets + zstandard) theme={null}
  import asyncio
  import json
  import zstandard
  import websockets

  async def main():
      uri = "wss://api.struct.to/ws?api-key=YOUR_API_KEY"
      dctx = zstandard.ZstdDecompressor()
      async with websockets.connect(uri, additional_headers={"X-Ws-Compression": "zstd"}) as ws:
          async for frame in ws:
              raw = dctx.decompress(frame) if isinstance(frame, bytes) else frame
              msg = json.loads(raw)
              print(msg["type"], msg.get("message"))

  asyncio.run(main())
  ```
</CodeGroup>

<Note>
  Browsers have no native zstd decoder (`DecompressionStream` supports only gzip and deflate), so a browser client needs a small library such as [`fzstd`](https://www.npmjs.com/package/fzstd) to decompress frames. The TypeScript SDK handles this for you.
</Note>

Branch on the frame type rather than assuming every frame is binary. Text control frames such as the `{"type":"pong"}` keepalive reply still arrive as text, so a `typeof data === "string"` (or `isBinary` / `instanceof ArrayBuffer`) check keeps both paths working on the same connection.

## Confirming the negotiated mode

Each room's `*_subscribe_response` echoes a `compression` field reporting whether zstd delivery is active for the connection, so you can assert the negotiation succeeded without inspecting frame types.

```json theme={null}
{
  "type": "trade_stream_subscribe_response",
  "message": { "compression": "zstd", "subscribe_all": false }
}
```

## When to use it

Compression trades a little CPU on both ends for lower bandwidth. It pays off most on high-volume subscriptions, verbose JSON payloads (metrics and order-book rooms), [firehose subscriptions](/websockets/firehose), and bandwidth-constrained or metered clients. For a handful of narrowly filtered rooms the savings are marginal, so leave it off unless you're moving real volume.

<Tip>
  The [dashboard websockets playground](https://www.struct.to/dashboard/websockets) has a **zstd compression** toggle on the connection card, so you can watch compressed delivery end to end before wiring it into your own client.
</Tip>

## Next steps

* [Firehose subscriptions](/websockets/firehose) — the highest-volume streams, where compression matters most.
* [WebSocket Pricing](/websockets/pricing) — per-message rates (compression doesn't change billing; you're billed per message, not per byte).
* [Getting Started](/websockets/getting-started) — the full Rooms protocol.
