{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreicn6qvqd7llntdyozfvrviqitu3x2smlvuz4m7nn6qnzrt4qpfrn4",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mohlw4ut76h2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreickx7qibtrzhlfguirqt7tvyp3iz2hwrfaovd246pl6cocwqvooqu"
    },
    "mimeType": "image/webp",
    "size": 77880
  },
  "path": "/bluewhale-quant-lab/polymarket-clob-websocket-in-python-real-time-order-book-without-polling-4e6n",
  "publishedAt": "2026-06-17T04:52:41.000Z",
  "site": "https://dev.to",
  "tags": [
    "python",
    "websocket",
    "tutorial",
    "trading",
    "my Amsterdam VPS"
  ],
  "textContent": "If your Polymarket bot polls `GET /book` in a loop, your view of the market is as stale as your interval — and you'll lose to anyone using the **WebSocket feed**. This tutorial builds a real-time local order book in Python from the CLOB WebSocket stream.\n\n##  Why WebSocket beats polling\n\n  * **Polling:** you ask every N ms → your data is up to N ms old, and you burn REST rate-limit budget.\n  * **WebSocket:** the server **pushes** changes → your information latency drops to roughly your network round-trip.\n\n\n\nFor a reactive strategy, this is the difference between trading the market that _is_ and the market that _was_.\n\n##  Connect and subscribe\n\n\n    import asyncio, json, websockets\n\n    WS_URL = \"wss://ws-subscriptions-clob.polymarket.com/ws/market\"\n\n    async def subscribe(token_ids):\n        async with websockets.connect(WS_URL, ping_interval=20, ping_timeout=20) as ws:\n            await ws.send(json.dumps({\"assets_ids\": token_ids, \"type\": \"market\"}))\n            async for raw in ws:\n                yield json.loads(raw)\n\n\n_(Confirm the exact URL, subscription payload, and message shapes against current Polymarket docs — they change.)_\n\n##  Maintain a local book from snapshot + deltas\n\nThe feed typically sends an initial **book snapshot** then incremental **price-change** messages. Apply them to a local structure:\n\n\n\n    class LocalBook:\n        def __init__(self):\n            self.bids = {}   # price -> size\n            self.asks = {}\n\n        def apply_snapshot(self, msg):\n            self.bids = {float(l[\"price\"]): float(l[\"size\"]) for l in msg.get(\"bids\", [])}\n            self.asks = {float(l[\"price\"]): float(l[\"size\"]) for l in msg.get(\"asks\", [])}\n\n        def apply_delta(self, msg):\n            for ch in msg.get(\"changes\", []):\n                side = self.bids if ch[\"side\"] == \"BUY\" else self.asks\n                price, size = float(ch[\"price\"]), float(ch[\"size\"])\n                if size == 0:\n                    side.pop(price, None)\n                else:\n                    side[price] = size\n\n        def best_bid(self): return max(self.bids) if self.bids else None\n        def best_ask(self): return min(self.asks) if self.asks else None\n        def mid(self):\n            b, a = self.best_bid(), self.best_ask()\n            return (b + a) / 2 if b and a else None\n\n\n##  Wire it together\n\n\n    async def run(token_id):\n        book = LocalBook()\n        async for msg in subscribe([token_id]):\n            t = msg.get(\"event_type\") or msg.get(\"type\")\n            if t in (\"book\", \"snapshot\"):\n                book.apply_snapshot(msg)\n            elif t in (\"price_change\", \"delta\"):\n                book.apply_delta(msg)\n            # react immediately on fresh state\n            m = book.mid()\n            if m is not None:\n                on_mid(m, book)\n\n    asyncio.run(run(TOKEN_ID))\n\n\n##  Measure your information lag\n\nAlways know how fresh your book is:\n\n\n\n    import time\n    lag_ms = (time.time() - float(msg[\"timestamp\"])) * 1000\n    if lag_ms > 50:\n        print(f\"⚠ stale: {lag_ms:.0f}ms behind server\")\n\n\n##  The ceiling you can't code around\n\nHere's the catch: your information latency can never beat your **network round-trip to the server**. The CLOB WebSocket terminates in **Amsterdam** — I measured ~1.2 ms from an AMS box vs ~88 ms from US-East. From the US, even a perfect WebSocket implementation is ~90 ms behind reality, because that's how long the packets take to arrive.\n\nSo the prerequisite for a fast feed is a server _near_ the feed. I run mine on an Amsterdam-metro VPS: **my Amsterdam VPS**\n_Disclosure: affiliate link, I earn a referral. It's the box behind the 1.2 ms._\n\n##  Robustness checklist\n\n  * **Reconnect with backoff** — WebSockets drop; resubscribe and re-snapshot on reconnect.\n  * **Detect gaps** — if deltas reference prices you don't have, request a fresh snapshot.\n  * **Heartbeat** — use `ping_interval` so dead connections are detected fast.\n  * **One process, many assets** — subscribe to multiple `assets_ids` on one socket.\n\n\n\n##  Recap\n\nWebSocket + a maintained local book gives you near-real-time perception. But the floor on \"real-time\" is your distance to Amsterdam. Get the server location right, then this code makes you genuinely fast.\n\n_Code is illustrative — verify against current docs. Latency from my own 2026 tests. Not financial advice._",
  "title": "Polymarket CLOB WebSocket in Python — Real-Time Order Book Without Polling"
}