Indexing the Polymarket CLOB WebSocket Order Feed in Real Time
Polymarket's Central Limit Order Book pushes order events over WebSocket at high frequency — this tutorial shows how to build a persistent Postgres indexer with sub-second lag, handle reconnects, and backfill gaps using the REST snapshot endpoint.
Polymarket's Central Limit Order Book WebSocket order feed is the raw nerve of every serious bot running on the platform. If you are quoting markets, running arbitrage across outcomes, or just want a reliable view of the book, consuming this feed correctly — with reconnect logic, gap detection, and a durable write path — is non-negotiable. This post walks through exactly how to build that in production.
What the CLOB WebSocket Actually Sends
Polymarket's CLOB is operated by CLOB API (clob.polymarket.com). The WebSocket endpoint lives at wss://ws-subscriptions-clob.polymarket.com/ws/. You subscribe per-market by sending a JSON subscription message with the token IDs you care about and the channel type: either market (price level updates) or user (your own order lifecycle).
For an indexer, you want the market channel. Each event is a JSON object with a type field that is one of:
book— full order book snapshot, sent on subscribe and periodicallyprice_change— a diff: a list of bids and asks with updated quantities at specific price levelslast_trade_price— the most recent fill price
The key thing to understand is that price_change events are diffs, not full books. A price level with size: "0" means that level has been wiped. You must apply diffs onto your local order book state in sequence. If you miss events — due to a reconnect, a slow consumer, or a network blip — your local book is wrong, silently.
Schema Design for Sub-Second Writes
The write path needs to keep up with the feed without becoming a bottleneck. In practice, Polymarket's CLOB can push dozens of events per second across active markets. The schema that works in production has two tables:
CREATE TABLE clob_book_snapshots (
id BIGSERIAL PRIMARY KEY,
token_id TEXT NOT NULL,
captured_at TIMESTAMPTZ NOT NULL DEFAULT now(),
bids JSONB NOT NULL,
asks JSONB NOT NULL
);
CREATE TABLE clob_events (
seq BIGINT NOT NULL,
token_id TEXT NOT NULL,
event_type TEXT NOT NULL, -- 'price_change' | 'book' | 'last_trade_price'
payload JSONB NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (token_id, seq)
);
CREATE INDEX ON clob_events (token_id, received_at DESC);
Write the raw payload as JSONB and parse it on read. This avoids schema migrations every time the feed changes a field name, and Postgres's JSONB indexing handles selective queries well. For snapshot captures, bids and asks are stored as [["0.62", "150"], ...] arrays — price-as-string to avoid floating-point drift.
Keep your insert path async. Use a channel or queue in your application layer so the WebSocket read loop never blocks on a database write. A Node.js worker thread writing to Postgres via pg with COPY streaming, or a Go goroutine batching inserts every 50ms, both work well. The goal is to keep end-to-end lag — event received to row committed — under 200ms.
Reconnect Logic and the Snapshot Backfill Pattern
WebSocket connections drop. The CLOB server does rolling restarts, your cloud box has a network hiccup, or you hit Cloudflare's idle timeout. When you reconnect, you do not know which events you missed. The correct pattern:
- On (re)connect, immediately send the subscribe message.
- The server will respond with a
booksnapshot event — that is your new ground truth. - Query the REST snapshot endpoint to cross-check:
GET https://clob.polymarket.com/book?token_id=<id>. This returns the current best bid/ask and total size at each level. - Diff your received
bookevent against the REST snapshot. If they disagree, use the REST snapshot — it is authoritative. - Mark a
reconnected_attimestamp in your state. Any application logic that depends on sequential event history should be aware of this gap.
The tricky edge case is a reconnect that happens between a price_change event being emitted and you receiving it. Your sequence counter will have a hole. Store the seq field from each event (hash in Polymarket's event envelope) and alert if you see a non-monotonic jump. On detection, trigger a REST snapshot refresh and reset your local book state.
Handling Sequence Gaps in Practice
Polymarket's WebSocket events include an asset_id (the token ID) but not always an explicit monotonic sequence number in the price_change payloads — the hash field is an event identifier, not a gap-detection counter. To detect gaps, you rely on wall-clock continuity: if your consumer goes silent for more than a configurable threshold (say, 5 seconds on a liquid market that normally ticks every 200ms), assume you have missed events and trigger a REST refresh.
A reliable implementation runs a watchdog timer per subscribed token:
const STALE_THRESHOLD_MS = 5_000;
function resetWatchdog(tokenId: string) {
clearTimeout(watchdogs.get(tokenId));
watchdogs.set(tokenId, setTimeout(() => {
log.warn({ tokenId }, "feed stale — refreshing snapshot");
refreshSnapshot(tokenId); // hits the REST endpoint, resets local book
}, STALE_THRESHOLD_MS));
}
Call resetWatchdog on every received event for that token. On liquid markets the timer never fires. On quiet markets you may want to lengthen the threshold or send a periodic ping frame to confirm the connection is alive.
Operational Considerations: Latency Numbers and Resource Use
A single-process Node.js indexer subscribed to 50 token IDs (roughly 25 binary markets) typically sits at 15–30MB RSS and handles the feed comfortably on a single core. Average write latency to a local Postgres instance is 2–5ms per batch insert when you batch 10–20 rows at a time. End-to-end lag from CLOB event to committed row is consistently under 100ms on a VPS in us-east with the Polymarket server also in us-east.
For larger deployments — say, monitoring 500+ markets for a Polymarket market-making strategy — the architecture shifts: a single WebSocket consumer process per subscription group writes to an in-memory ring buffer, and a separate writer process drains it in bulk. This keeps the consumer's hot path free of any I/O. If you are building out data infrastructure to support active bots, this separation is worth doing from the start rather than retrofitting under load.
One often-overlooked cost: Postgres table bloat. clob_events grows fast. On a busy market, you can accumulate several million rows per day. Run a partition by day on received_at and set up a retention policy — in production we typically keep 7 days of raw events and rely on daily snapshots for anything older. Postgres 14+ native partitioning handles this cleanly with PARTITION BY RANGE (received_at).
What You Are Actually Building Toward
The indexer is not the end goal — it is the substrate everything else runs on. A Polymarket spread bot needs a reliable book state to quote against; a cross-market arbitrage bot needs to compare multiple books in real time. Neither is possible without a feed consumer that handles reconnects correctly and keeps lag under a latency budget you have actually measured.
The gap between "I consume the WebSocket" and "I have a production-reliable indexer" is almost entirely in the reconnect and backfill logic. Get that right first, measure your write latency under load, and the trading strategies built on top become dramatically simpler to reason about.
If you need a production Polymarket indexer or a full bot stack built on top of it, reach out — this is exactly the kind of infrastructure TierZero ships.
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