Short answer: A WebSocket order book is a local copy of an exchange's bid and ask levels that your program keeps current from a stream. On Binance you buffer diff-depth events, fetch a REST snapshot, discard events already contained in it and apply the rest in update-ID order, rebuilding from scratch whenever an ID gap appears. Some exchanges, such as Kraken, also send a CRC32 checksum so you can verify the top of your book.
Calculate it with your own figures: Slippage calculator for trading.
Spread monitors, execution algorithms and arbitrage scanners need the current order book. Polling REST is slow and burns rate limits; streaming changes is fast but unforgiving, because one missed message silently corrupts every number derived from the book.
This guide follows Binance's documented synchronization procedure as of October 2026, compares it with Kraken, OKX, Bybit and MEXC, and includes Python that was tested on public BTCUSDT data recorded on 5 November 2026. It is an engineering guide, not trading advice.
Crypto order book API: REST or WebSocket?
Use the REST order book API for an occasional snapshot and a WebSocket order book for anything that must stay current. Most local books combine them; on Binance the snapshot comes from GET /api/v3/depth.
| Question | REST order book API | WebSocket stream |
|---|---|---|
| What you get | All requested levels at one moment | Changes as they happen |
| Failure mode | Stale between calls | One missed message corrupts the book until it is rebuilt |
Where the first full book comes from differs by exchange, so a Binance sync routine does not carry over unchanged:
| Exchange | Starting snapshot | Continuity check |
|---|---|---|
| Binance Spot | REST GET /api/v3/depth, merged with buffered stream events | U, u, lastUpdateId |
| Bybit v5 | First message on the stream, then deltas; a new snapshot means reset the local book | u; u = 1 marks a snapshot after a service restart |
| MEXC Spot | REST GET /api/v3/depth, merged with spot@public.aggre.depth.v3.api.pb@100ms@SYMBOL events | fromVersion, toVersion |
OKX v5 books | First push on the stream, 400 levels, then increments every 100 ms | seqId, prevSeqId |
A tested Bybit version is in the Bybit API in Python.
Order book snapshot and diff: how depth streams work
A depth stream sends changes, not the whole book, so you need a starting snapshot plus every later change, in order. Binance's partial depth streams (btcusdt@depth5, @depth10, @depth20) push only the top levels and suffice if you need the best few prices. For a full book you use the diff depth stream, btcusdt@depth or btcusdt@depth@100ms:
| Field | Meaning |
|---|---|
U | First update ID in the event |
u | Final update ID in the event |
b, a | Bid and ask levels as [price, quantity] |
E | Event time in milliseconds |
The quantity is the new absolute size at that price, not a change; zero means the level is gone. The snapshot comes from GET /api/v3/depth, which returns up to 5,000 levels per side plus a lastUpdateId, at a weight of 5 for up to 100 levels rising to 250 for 1,001 to 5,000, out of 6,000 per minute per IP.
Binance depth stream order book: the official procedure
Binance documents the synchronization precisely. In summary, as of October 2026:
- Open the diff depth stream and buffer its events, noting the
Uof the first one. - Fetch a snapshot from
/api/v3/depthwithlimit=5000. - If the snapshot's
lastUpdateIdis lower than that firstU, fetch the snapshot again. - Discard buffered events whose
uis less than or equal tolastUpdateId. - Set the local book to the snapshot, with update ID
lastUpdateId. - Apply the remaining buffered events, then every new event, with the update rules below.
For each event: ignore it if u is lower than the book's update ID; if U is greater than the book's update ID plus one, events were missed and the book must be rebuilt; otherwise set each level to its new quantity, remove zero-quantity levels and set the book's update ID to u.
Because a snapshot holds at most 5,000 levels per side, Binance warns that deeper levels stay unknown until they change.
Update IDs, gap detection and resync
A gap means your book is wrong, and the only safe response is to rebuild it from a fresh snapshot. Gaps come from dropped connections, from Binance closing every connection at the 24-hour mark or before a server shutdown, and from consumers that fall behind and lose messages.
A practical resync keeps the stream buffering while a new snapshot is fetched, then replays the buffer through the same rules. Rate-limit the rebuild loop, because a bug that resyncs on every message can exhaust the IP weight budget and trigger HTTP 429 responses. Log every resync with its cause; frequent gaps usually point to a slow consumer. Connection limits and a tested reconnect loop are in the Binance WebSocket API guide.
A tested L2 order book in Python
A local book can be tested on a short recording of public data, which this script makes with no API key (pip install requests websockets):
import asyncio, json, requests, websockets
STREAM = "wss://data-stream.binance.vision/ws/btcusdt@depth@100ms"
REST = "https://data-api.binance.vision/api/v3/depth"
async def record(n=60, snapshot_after=3):
events, snapshot = [], None
async with websockets.connect(STREAM) as ws:
while len(events) < n:
events.append(json.loads(await ws.recv()))
if len(events) == snapshot_after:
snapshot = await asyncio.to_thread(lambda: requests.get(
REST, params={"symbol": "BTCUSDT", "limit": 5000}, timeout=10).json())
with open("depth_recording.json", "w") as fh:
json.dump({"snapshot": snapshot, "events": events}, fh)
asyncio.run(record())
The sketch below implements the procedure on such a recording: 60 events from wss://data-stream.binance.vision/ws/btcusdt@depth@100ms captured over about six seconds, plus a 5,000-level snapshot fetched after the third event, all public data recorded on 5 November 2026 and saved as depth_recording.json with snapshot and events keys. It then deletes one message to show that the gap is caught.
import json
from decimal import Decimal
class GapError(Exception):
"""Raised when an update was missed and the book must be rebuilt."""
class LocalBook:
def __init__(self, snapshot):
self.update_id = snapshot["lastUpdateId"]
self.bids = {Decimal(p): Decimal(q) for p, q in snapshot["bids"]}
self.asks = {Decimal(p): Decimal(q) for p, q in snapshot["asks"]}
def apply(self, ev):
if ev["u"] < self.update_id: # already contained in the book
return False
if ev["U"] > self.update_id + 1: # hole in the sequence
raise GapError(f"expected {self.update_id + 1}, got U={ev['U']}")
for book, levels in ((self.bids, ev["b"]), (self.asks, ev["a"])):
for price, qty in levels:
price, qty = Decimal(price), Decimal(qty)
if qty == 0:
book.pop(price, None) # zero quantity removes the level
else:
book[price] = qty # absolute quantity, not a delta
self.update_id = ev["u"]
return True
def build(snapshot, buffered):
if snapshot["lastUpdateId"] < buffered[0]["U"]:
raise GapError("snapshot older than first buffered event: fetch it again")
book = LocalBook(snapshot)
applied = sum(book.apply(e) for e in buffered if e["u"] > snapshot["lastUpdateId"])
return book, applied
def buy_cost(book, qty):
"""Walk the asks to estimate the average fill price of a market buy."""
left, cost = Decimal(qty), Decimal(0)
for price in sorted(book.asks):
take = min(left, book.asks[price])
cost, left = cost + take * price, left - take
if left == 0:
return cost / Decimal(qty)
raise ValueError("not enough depth in the local book")
rec = json.load(open("depth_recording.json"))
book, applied = build(rec["snapshot"], rec["events"])
bid, ask = max(book.bids), min(book.asks)
print("applied", applied, "events; update id", book.update_id)
print("best bid", format(bid.normalize(), "f"), "best ask", format(ask.normalize(), "f"),
"spread", format((ask - bid).normalize(), "f"))
avg = buy_cost(book, "20") # hypothetical order size
print("20 BTC market buy: avg", round(avg, 2), "slippage bps", round((avg / ask - 1) * 10000, 2))
broken = rec["events"][:]
del broken[len(broken) // 2] # simulate one lost message
try:
build(rec["snapshot"], broken)
except GapError as err:
print("resync needed:", err)
Output on the recorded sample (prices from that moment; your numbers will differ):
applied 45 events; update id 102326453916
best bid 80582.01 best ask 80582.02 spread 0.01
20 BTC market buy: avg 80598.25 slippage bps 2.01
resync needed: expected 102326451244, got U=102326451257
Fifteen buffered events predated the snapshot and were skipped. Prices are Decimal keys, so no float rounding creeps in. In a live system a GapError triggers the resync described above.
Order book checksums and sequence IDs: Kraken, OKX
Binance's diff stream has no checksum, so update IDs are its integrity check. Kraken's WebSocket v2 book channel (wss://ws.kraken.com/v2, depth of 10, 25, 100, 500 or 1,000 levels, default 10) sends a snapshot followed by updates, and every message carries a CRC32 checksum of the top 10 bids and asks. You concatenate the top 10 asks (low to high) and bids (high to low), each price and quantity without the decimal point and leading zeros, and compare the CRC32 of that string. After each update you must also truncate the book to your subscribed depth, because levels leaving the range are not sent with quantity zero. This function reproduces the worked example in Kraken's guide:
import zlib
from decimal import Decimal
def kraken_checksum(bids, asks):
"""CRC32 over the top 10 asks (low to high) then top 10 bids (high to low)."""
def fmt(levels):
return "".join(p.replace(".", "").lstrip("0") + q.replace(".", "").lstrip("0")
for p, q in levels)
top_asks = sorted(asks, key=lambda lv: Decimal(lv[0]))[:10]
top_bids = sorted(bids, key=lambda lv: Decimal(lv[0]), reverse=True)[:10]
return zlib.crc32((fmt(top_asks) + fmt(top_bids)).encode())
# Example book from Kraken's checksum guide (prices and quantities as strings)
bids = [("45283.5", "0.10000000"), ("45283.4", "1.54582015"), ("45282.1", "0.10000000"),
("45281.0", "0.10000000"), ("45280.3", "1.54592586"), ("45279.0", "0.07990000"),
("45277.6", "0.03310103"), ("45277.5", "0.30000000"), ("45277.3", "1.54602737"),
("45276.6", "0.15445238")]
asks = [("45285.2", "0.00100000"), ("45286.4", "1.54571953"), ("45286.6", "1.54571109"),
("45289.6", "1.54560911"), ("45290.2", "0.15890660"), ("45291.8", "1.54553491"),
("45294.7", "0.04454749"), ("45296.1", "0.35380000"), ("45297.5", "0.09945542"),
("45299.5", "0.18772827")]
print(kraken_checksum(bids, asks), kraken_checksum(bids, asks) == 3310070434)
It prints 3310070434 True. Parse prices as strings or decimals, as Kraken advises, because floats drop the trailing zeros the checksum depends on. On a mismatch, Kraken says to unsubscribe and resubscribe for a fresh snapshot.
| Exchange | Mechanism |
|---|---|
| Binance Spot | U and u update IDs; a gap forces a rebuild |
| Kraken WebSocket v2 | CRC32 of the top 10 levels in every message |
OKX v5 books | Checksum fixed at 0 since 23 June 2026; each message's prevSeqId must equal the previous seqId |
| Coinbase Advanced Trade | Per-product sequence numbers; a jump of more than one means a dropped message |
OKX snapshots carry a prevSeqId of -1, an update with empty sides and an unchanged seqId is a keep-alive sent after about 60 seconds without changes, and a seqId lower than prevSeqId marks a reset after maintenance. Code that still validates OKX checksums would now flag nearly every message, a reminder to re-read the docs. Signing and streams on each platform are in the Kraken API in Python and the OKX API in Python.
Data structures, latency and throughput
The sketch sorts dictionaries on demand, which is clear but costs a sort per best-price lookup. Production code keeps prices sorted, for example with Python's bisect module, or stores integer tick offsets in arrays, which also makes comparisons exact.
Keep the receive loop to reading and queueing, and apply updates in a separate task. A blocked reader delays replies to the server's pings, which Binance sends every 20 seconds and expects answered within a minute. Compare each event's E timestamp with your receive time to measure feed latency.
How to read a level 2 order book: spread, depth and slippage
A level 2 order book lists the total size at each price, not the individual orders, and reading it means turning it into the numbers trading logic needs: the spread is the best ask minus the best bid, depth is the quantity available within a price band, and expected slippage comes from walking the levels for a given order size, as buy_cost does above.
Hypothetical example. On the recorded BTCUSDT book, a 20 BTC market buy would have consumed the 0.23425 BTC at the best ask of 80,582.02 and continued up the book, for an average of about 80,598.25, roughly 2.01 basis points above the best ask, before fees.
Treat such estimates as guesses: the book changes before your order arrives and hidden liquidity is invisible. Arbitrage scanners compare these estimates across venues, which is why visible spreads often vanish after costs, as explained in crypto arbitrage bots and the multi-exchange arbitrage monitoring scenario. The surrounding plumbing (authentication, limits, reconnects) is covered in crypto exchange API integration; for a built system, see exchange API integration.
For readers in Turkey, Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized.
Frequently asked questions
What is level 2 order book data?
It lists the total quantity resting at each price level on both sides of the book. It does not show the individual orders behind each level, and exchanges limit how many levels they publish.
Why not just poll the REST depth endpoint?
Polling is slow and expensive: a 5,000-level Binance snapshot costs 250 of the 6,000 weight available per minute per IP, while a stream delivers every change.
What does a quantity of zero mean in a depth update?
The price level no longer exists and must be removed. Non-zero quantities replace the previous size; they are not added to it.
What should I do when I detect a gap in update IDs?
Discard the local book, keep buffering stream events, fetch a new snapshot and replay the buffer. Never try to patch the gap, because you cannot know which levels changed.
Does Binance provide an order book checksum?
Not on the Spot diff depth stream as of October 2026; consistency relies on the U and u update IDs. Kraken sends a CRC32 checksum of the top 10 levels, while OKX deprecated its checksum in June 2026 in favor of sequence IDs.
Is the 100 ms depth stream always better?
No. If your strategy reacts on a slower cycle, the extra updates only add processing load.
Sources
- Binance Spot API documentation: WebSocket Streams (diff depth, local order book procedure) (opens in a new tab)
- Binance Spot API documentation: REST API (order book endpoint and weights) (opens in a new tab)
- Binance Spot API documentation: Market Data Only URLs (opens in a new tab)
- Kraken: Spot WebSocket v2 Book (Level 2) channel (opens in a new tab)
- Kraken: Book checksum (WebSocket v2) (opens in a new tab)
- Kraken: WebSocket reconnection and resilience (opens in a new tab)
- OKX API v5 documentation: Order book channel (seqId, prevSeqId, deprecated checksum) (opens in a new tab)
- OKX API v5 changelog: Deprecation of checksum field in order book channels, 2026-06-23 (opens in a new tab)
- Coinbase Advanced Trade WebSocket overview (sequence numbers) (opens in a new tab)
- Python documentation: bisect, array bisection algorithm (opens in a new tab)
- Python documentation: decimal, decimal fixed-point arithmetic (opens in a new tab)
- Bybit API v5: Orderbook WebSocket stream (snapshot, delta, u = 1) (opens in a new tab)
- MEXC Spot API: How to properly maintain a local copy of the order book (opens in a new tab)
This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the editorial policy.