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
hashvalues. 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. Thewebsocketslibrary enables it by default, but verify withss -tnithatnonagleis active on your socket.- Receive buffer size:
net.core.rmem_max=134217728and matchingSO_RCVBUFon 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 projectMore from the blog
Public vs Paid RPC: How to Run a Fair Benchmark
A fair comparison of public and paid RPC endpoints must account for quotas, workload, freshness and support guarantees.
Read articleRPC Benchmark Percentiles Explained: p50, p95 and p99
Why averages hide the latency spikes that break trading bots, wallets and production blockchain applications.
Read articleHyperliquid REST vs WebSocket Benchmark: What to Measure
A benchmark plan for Hyperliquid market data that separates request latency from streaming freshness and recovery.
Read article