All posts
API ReferenceWebSocketsPolymarket APITrading Bots

Polymarket WebSockets: Real-Time Data That Does Not Drop

Polling the order book is the most common way to burn a rate limit budget for no benefit. Here are the four streams, the subscription options nobody documents, and the failure mode where your bot receives silence and calls it calm.

10 min read

If your bot polls GET /book on a timer, this page will save you most of your rate limit budget and a chunk of latency. Polymarket pushes book updates over WebSocket, and a subscription costs you one connection instead of thousands of requests.

The four streams

StreamURLCarriesAuth
Marketwss://ws-subscriptions-clob.polymarket.com/ws/marketPublic book, price and lifecycle updatesNone
Userwss://ws-subscriptions-clob.polymarket.com/ws/userYour order and trade updatesYes
RTDSwss://ws-live-data.polymarket.comReference prices, comments, trade activityNone
Sportswss://sports-api.polymarket.com/wsLive game status and scoresNone

Subscribing to the market channel

Connect, then send one JSON subscription message. assets_ids takes token IDs — the same ones you get from market metadata — and type must be "market".

{
  "assets_ids": ["65818619657568813474341868652308942079804919287380422192892211131408793125422"],
  "type": "market",
  "initial_dump": true,
  "level": 2,
  "custom_feature_enabled": true
}
FieldRequiredDefaultWhat it does
assets_idsYesToken IDs to subscribe to
typeYesMust be "market"
initial_dumpNotrueSend a full book snapshot on subscribe
levelNo2Subscription level — 1, 2 or 3
custom_feature_enabledNofalseEnables best_bid_ask, new_market and market_resolved events

Keep initial_dump: true unless you have a specific reason not to. Without it you start with no book state and have to wait for incremental updates to reconstruct one, which means your bot begins life with an incomplete picture.

Message types

Every message carries an event_type. Branch on it exhaustively — an unrecognised type should be logged, not silently dropped, because that is how you find out the protocol changed.

event_typeMeaningNeeds opt-in
bookFull order book snapshotNo
price_changeIncremental price/level updateNo
last_trade_priceA trade executedNo
tick_size_changeMarket's tick grid changedNo
best_bid_askTop-of-book updateYes
new_marketA market was createdYes
market_resolvedA market settledYes

Staying connected: PING every 10 seconds

This is the requirement that catches everyone. The client must send a PING every 10 seconds; the server replies PONG. Miss the heartbeat and the connection is dropped.

Note the direction: this is a client heartbeat. You are not waiting for the server to ping you. A library's built-in WebSocket keepalive at some other interval is not a substitute — send the application-level PING yourself.

import asyncio, json, time
import websockets

URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
STALE_AFTER = 30          # seconds without any message = treat as dead


async def stream(token_ids, on_event):
    backoff = 1
    while True:
        try:
            async with websockets.connect(URL) as ws:
                await ws.send(json.dumps({
                    "assets_ids": token_ids,
                    "type": "market",
                    "initial_dump": True,
                    "custom_feature_enabled": True,   # or lose 3 event types
                }))
                backoff = 1                            # reconnected cleanly
                last_msg = time.monotonic()

                async def heartbeat():
                    while True:
                        await asyncio.sleep(10)        # required cadence
                        await ws.send("PING")

                hb = asyncio.create_task(heartbeat())
                try:
                    while True:
                        # Silence is a failure mode, not a quiet market.
                        raw = await asyncio.wait_for(ws.recv(), timeout=STALE_AFTER)
                        last_msg = time.monotonic()
                        if raw == "PONG":
                            continue
                        for msg in json.loads(raw) if raw.startswith("[") else [json.loads(raw)]:
                            on_event(msg)
                finally:
                    hb.cancel()

        except Exception as exc:
            # Reconnect with capped exponential backoff + jitter.
            wait = min(backoff, 30) * (0.5 + 0.5 * asyncio.get_event_loop().time() % 1)
            print(f"ws down ({exc}); reconnecting in {wait:.1f}s")
            await asyncio.sleep(wait)
            backoff = min(backoff * 2, 30)

Changing subscriptions without reconnecting

Five-minute markets expire constantly, so a bot tracking "current BTC 5m markets" needs to change its subscription set continuously. You do not have to tear down the connection to do it — the market channel supports subscribing and unsubscribing on a live connection.

This matters more than it sounds. Reconnecting on every market rotation means repeatedly paying connection setup cost, re-requesting initial dumps, and creating a window where you are receiving nothing at exactly the moment a new market opens.

The failure mode that costs money

A dropped WebSocket does not raise an exception. It stops delivering messages. To a naive bot, "no updates" is indistinguishable from "the market is not moving" — so a bot holding a position will sit on a stale price indefinitely, confident that nothing has changed.

  • Track the age of the last message. If nothing has arrived in ~30 seconds on an active market, you are disconnected, not calm. The asyncio.wait_for timeout above is doing exactly this job.
  • Treat staleness as an error condition. Stop quoting, flatten if your risk rules require it, and reconnect — do not keep trading on a frozen book.
  • Back off on reconnect, with jitter. A tight reconnect loop during an outage is how you get rate limited on top of being disconnected, and a fleet reconnecting in lockstep makes it worse.
  • Re-sync state after reconnecting. You missed updates while you were away. Take the fresh initial_dump as truth and discard your prior book, rather than merging incremental updates into a stale snapshot.
  • Reconcile positions after any gap. If you were disconnected from the User channel, you may have missed a fill. Re-read positions before acting on assumed state.

WebSockets and rate limits

A frequent question, and the answer is the point of the whole exercise: a subscription does not consume per-request rate limit budget the way polling does. Polymarket's documented limits are per-endpoint HTTP limits, and streaming sidesteps them almost entirely.

ApproachCost of tracking 20 markets for an hour
Poll /book every second72,000 requests against a 1,500 req/10s budget
Poll /books (batch) every second3,600 requests against a 500 req/10s budget
WebSocket subscriptionOne connection, plus 360 heartbeats
Polling also gives you a worse answer: you see the book as of your last poll, not as of the last change.

You will still make HTTP calls — market discovery, tick sizes, order placement, position reconciliation — and those remain subject to the documented rate limits. Streaming just removes the largest and least useful source of request volume.

What streams will not give you

One boundary worth stating plainly, because it is the thing people try next. A WebSocket gives you the book from the moment you subscribe. It cannot give you the book last Tuesday.

Polymarket keeps no historical order book. Once a market resolves, its book returns 404 and prices-history returns 200 with an empty array. If you want to test a strategy against how markets actually traded, you need snapshots somebody recorded at the time — PolyTest does that for crypto Up/Down markets, with 8 levels of depth per side and resolved markets retained. Or start your own collector today, accept that you begin with zero history, and wait.

Frequently asked questions

What is the Polymarket WebSocket URL?
The public market channel is wss://ws-subscriptions-clob.polymarket.com/ws/market and the authenticated user channel is wss://ws-subscriptions-clob.polymarket.com/ws/user. There are also two other streams: RTDS at wss://ws-live-data.polymarket.com for reference prices, comments and trade activity, and a sports feed at wss://sports-api.polymarket.com/ws.
Is there a rate limit on the Polymarket WebSocket?
A WebSocket subscription does not consume per-request rate limit budget the way HTTP polling does — Polymarket's documented limits are per-endpoint HTTP limits. Streaming is specifically the recommended way to avoid them. The binding requirement on a connection is the heartbeat: send a PING every 10 seconds or the connection is dropped. HTTP calls you still make for discovery, tick sizes and order placement remain subject to the normal limits.
How do I keep a Polymarket WebSocket connection alive?
Send a PING message every 10 seconds; the server responds with PONG. This is a client-side application-level heartbeat, so a WebSocket library's built-in keepalive at a different interval is not a substitute. Also track the age of the last received message — a dropped connection stops delivering data without raising an error, so treat roughly 30 seconds of silence on an active market as a disconnect rather than a quiet market.
Why am I not receiving best_bid_ask or market_resolved events?
Those events are off by default. Set custom_feature_enabled to true in your subscription message to enable best_bid_ask, new_market and market_resolved. There is no error or warning when the flag is absent — the events simply never arrive, which is easy to mistake for an inactive market.
Should my Polymarket bot use WebSockets or poll the REST API?
Use WebSockets for live book and price data. Tracking 20 markets by polling /book once per second costs 72,000 requests per hour against a 1,500 req/10s budget, while a subscription costs one connection plus heartbeats — and polling also returns a staler answer, since you see the book as of your last poll rather than as of the last change. Keep REST for market discovery, tick sizes, order placement and position reconciliation.
Can I change which markets I am subscribed to without reconnecting?
Yes. The market channel supports subscribing and unsubscribing to assets on a live connection, which matters for short-lived markets like 5-minute Up/Down contracts that rotate constantly. Reconnecting on every rotation repeatedly pays connection setup cost and leaves a gap in coverage at exactly the moment a new market opens.

Get the historical data Polymarket does not keep

PolyTest records Polymarket crypto Up/Down markets as they run — 90M+ snapshots with 8 levels of order book depth, sub-second timestamps, and resolved markets preserved. Free tier, no card.

Keep reading