All articles
Infrastructure·February 1, 2026·5 min read

Optimizing Polymarket CLOB WebSocket Feed Latency for Bots

Deep dive into Polymarket's central limit order book WebSocket API — message schema, reconnect behavior, and techniques for reducing the time between an order book update and your bot's response. Includes a Python reference client with sub-10ms processing loops.

Polymarket's CLOB runs on a permissioned off-chain matching engine built by CLOB Labs, with settlement on Polygon. The WebSocket feed is the only way to get real-time order book state — REST snapshots are stale by the time you parse them. If you are building a market-making or directional bot on Polymarket, every millisecond you shave off the path from message received to order placed directly determines whether you get filled at your price or chase a moved market.

The Message Schema You Actually Receive

The CLOB WebSocket endpoint is wss://ws-subscriptions-clob.polymarket.com/ws/. After the initial HTTP upgrade, the server expects a JSON subscription frame:

{
  "auth": {},
  "markets": ["0xabc123..."],
  "type": "market"
}

Incoming messages follow a discriminated-union shape keyed on "event_type". The two you care about are "book" (full snapshot, sent on subscription) and "price_change" (incremental delta). A delta message looks like:

{
  "event_type": "price_change",
  "asset_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
  "changes": [
    {"price": "0.64", "side": "BUY", "size": "1500.00"},
    {"price": "0.63", "side": "BUY", "size": "0.00"}
  ],
  "hash": "0xdeadbeef",
  "market": "0xabc123...",
  "timestamp": "1719100000123"
}

A size of "0.00" means the level was removed. The hash field is a Merkle root of the current book state — you can use it to detect missed deltas by comparing against the hash from the prior "book" snapshot. If they diverge you are out of sync; re-subscribe immediately.

Reconnect Behavior and Gap Detection

The server does not buffer messages during a disconnect. A reconnect always begins with a fresh "book" snapshot, which can be 10–40 KB for active markets. This means your gap-detection logic must work in two modes:

  • During normal operation: compare consecutive hash values. A gap means you received an out-of-order or dropped delta. Trigger a re-subscribe, not a REST fetch — REST snapshots carry additional HTTP round-trip overhead and are subject to rate limits.
  • After reconnect: throw away any in-flight order decisions computed from the stale book. Wait until the snapshot "book" message arrives, rebuild the full book in memory, and only then resume quoting.

Exponential backoff with jitter is non-negotiable here. Reconnect storms after a server blip will get your IP throttled. A reasonable floor is 250 ms initial delay, doubling up to 16 s, with ±20% jitter applied per attempt.

Python Reference Client with Sub-10ms Processing

The biggest latency killer in Python WebSocket clients is asyncio event loop contention. If your order logic blocks the loop for even 2–3 ms, you will fall behind the feed. The pattern that works in production: keep the WebSocket receive loop minimal — parse, enqueue — and run order logic in a separate thread or asyncio.Task that pulls from the queue.

import asyncio, json, time
from collections import defaultdict
from websockets.asyncio.client import connect

ENDPOINT = "wss://ws-subscriptions-clob.polymarket.com/ws/"

class CLOBBook:
    def __init__(self):
        self.bids: dict[str, float] = {}  # price -> size
        self.asks: dict[str, float] = {}

    def apply_snapshot(self, bids, asks):
        self.bids = {b["price"]: float(b["size"]) for b in bids}
        self.asks = {a["price"]: float(a["size"]) for a in asks}

    def apply_delta(self, changes, side_key):
        book = self.bids if side_key == "BUY" else self.asks
        for ch in changes:
            sz = float(ch["size"])
            if sz == 0.0:
                book.pop(ch["price"], None)
            else:
                book[ch["price"]] = sz

async def stream(market_token: str, on_update):
    sub = json.dumps({"auth": {}, "markets": [market_token], "type": "market"})
    book = CLOBBook()
    async for ws in connect(ENDPOINT, max_size=2**22):
        try:
            await ws.send(sub)
            async for raw in ws:
                t0 = time.perf_counter_ns()
                msg = json.loads(raw)
                et = msg.get("event_type")
                if et == "book":
                    book.apply_snapshot(msg.get("bids", []), msg.get("asks", []))
                elif et == "price_change":
                    for ch_group in [msg.get("changes", [])]:
                        side = ch_group[0]["side"] if ch_group else None
                        if side:
                            book.apply_delta(ch_group, side)
                elapsed_us = (time.perf_counter_ns() - t0) / 1_000
                await on_update(book, elapsed_us)
        except Exception:
            await asyncio.sleep(0.25)

On a co-located VPS in the same AWS us-east-1 region as the Polymarket WebSocket servers, the json.loads + book-update path consistently completes in under 400 µs. on_update then has the remaining budget before the next message arrives.

Network Placement and OS-Level Tuning

Co-location matters more than code quality at the margins. The CLOB WebSocket servers resolve to AWS us-east-1 (3.220.x.x range as of mid-2026). A VPS in the same region will give you ~0.3 ms RTT; running from Europe adds 80–100 ms of irreducible physics.

At the OS level on Linux, two settings that actually move the needle:

  • TCP_NODELAY: set it. The websockets library enables it by default, but verify with ss -tni that nonagle is active on your socket.
  • Receive buffer size: net.core.rmem_max=134217728 and matching SO_RCVBUF on the socket. A 128 MB receive buffer prevents kernel drops during burst message periods without adding any userspace overhead.

On Python 3.12+ with asyncio event loops, switching the event loop policy to uvloop (asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())) reduces per-iteration overhead by 30–40% compared to the default selector loop. It is a one-line change with significant payoff.

Order Dispatch and Round-Trip Accounting

Once your book is updated, order placement goes through Polymarket's REST API (POST /order), not the WebSocket. That means a separate HTTPS connection, which you should keep alive and pre-warmed with an idle keepalive request every 10 s. Without this, TLS re-handshake adds 40–120 ms to your first order after a quiet period.

Track your round-trip time explicitly:

t_book_update → t_decision → t_order_sent → t_fill_confirmation

Log all four timestamps. In practice, the t_decision → t_order_sent gap is where most tuning work pays off — this is your order logic, risk checks, and serialization. Keep it under 1 ms. Anything above 5 ms in this segment usually means you are doing blocking I/O or lock contention inside the hot path.


If you want this running in production with monitoring, automatic re-hedging, and position limits already wired in, reach out — this is exactly the kind of system we build at TierZero.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#infrastructure#polymarket#websocket#trading-bots#clob#latency#python