# Local order book via WebSocket: snapshot and diff sync

URL: https://fincodelabs.com/en/blog/websocket-order-book · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Exchange APIs · Published: 2026-10-24 · Updated: 2026-11-05

How to maintain a local order book from WebSocket streams: Binance snapshot plus diff sync, update IDs, gap detection, Kraken checksums, tested Python.

**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.

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`.

**REST order book API and WebSocket stream compared**

| 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:

**Starting snapshot by exchange, read on 4 November 2026**

| 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](https://fincodelabs.com/en/blog/bybit-api-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`:

**Binance depthUpdate event fields**

| 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:

1. Open the diff depth stream and buffer its events, noting the `U` of the first one.
2. Fetch a snapshot from `/api/v3/depth` with `limit=5000`.
3. If the snapshot's `lastUpdateId` is lower than that first `U`, fetch the snapshot again.
4. Discard buffered events whose `u` is less than or equal to `lastUpdateId`.
5. Set the local book to the snapshot, with update ID `lastUpdateId`.
6. 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](https://fincodelabs.com/en/blog/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`):

```python
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.

```python
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:

```python
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.

**Book integrity checks, as of October 2026**

| 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](https://fincodelabs.com/en/blog/kraken-api-python) and [the OKX API in Python](https://fincodelabs.com/en/blog/okx-api-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](https://fincodelabs.com/en/blog/crypto-arbitrage-bot) and the [multi-exchange arbitrage monitoring](https://fincodelabs.com/en/projects/multi-exchange-arbitrage-monitoring) scenario. The surrounding plumbing (authentication, limits, reconnects) is covered in [crypto exchange API integration](https://fincodelabs.com/en/blog/crypto-exchange-api-integration); for a built system, see [exchange API integration](https://fincodelabs.com/en/services/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)](https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md)
- [Binance Spot API documentation: REST API (order book endpoint and weights)](https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md)
- [Binance Spot API documentation: Market Data Only URLs](https://github.com/binance/binance-spot-api-docs/blob/master/faqs/market_data_only.md)
- [Kraken: Spot WebSocket v2 Book (Level 2) channel](https://docs.kraken.com/exchange/api-reference/spot-websocket-v2/book)
- [Kraken: Book checksum (WebSocket v2)](https://docs.kraken.com/exchange/guides/websockets/book-checksum-v2)
- [Kraken: WebSocket reconnection and resilience](https://docs.kraken.com/exchange/guides/websockets/reconnection)
- [OKX API v5 documentation: Order book channel (seqId, prevSeqId, deprecated checksum)](https://www.okx.com/docs-v5/en/)
- [OKX API v5 changelog: Deprecation of checksum field in order book channels, 2026-06-23](https://www.okx.com/docs-v5/log_en/)
- [Coinbase Advanced Trade WebSocket overview (sequence numbers)](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/websocket/websocket-overview)
- [Python documentation: bisect, array bisection algorithm](https://docs.python.org/3/library/bisect.html)
- [Python documentation: decimal, decimal fixed-point arithmetic](https://docs.python.org/3/library/decimal.html)
- [Bybit API v5: Orderbook WebSocket stream (snapshot, delta, u = 1)](https://bybit-exchange.github.io/docs/v5/websocket/public/orderbook)
- [MEXC Spot API: How to properly maintain a local copy of the order book](https://www.mexc.com/api-docs/spot-v3/websocket-market-streams/how-to-properly-maintain-a-local-copy-of-the-order-book)

> 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](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/websocket-order-book

Cite as: Fincode, "Local order book via WebSocket: snapshot and diff sync", updated November 5, 2026, https://fincodelabs.com/en/blog/websocket-order-book
