SBE Level 50 Integration
Overview
- Channel: private MMWS only (not available on public WS).
- Topic:
ob.50.sbe.{symbol}(snapshot or delta, every 20 ms). - Format: SBE binary frames (
opcode = 2), little-endian. - Depth: 50 levels per side (no RPI in this stream).
- Units: timestamps in microseconds (µs); price/size are mantissas with exponents.
Release windows
- Spot testnet: Nov 25th 7am UTC+0
- Spot mainnet: Dec 2nd 7am UTC+0
- Futures testnet: Dec 9th 7am UTC+0
- Futures mainnet: Dec 16th 7am UTC+0
Order Book Update Logic (Snapshot + Delta Mode)
Message Types
Order book update streams contain two message types:
| Type | Meaning |
|---|---|
| snapshot | A full 50-level orderbook image. Must reset the local book. |
| delta | Incremental updates. Must apply changes to the existing book. |
Rules for the u (Update ID) Field
Behavior of u
- Field
uincreases monotonically for all snapshots and deltas. - Field
udoes not reset, unless there is a system reset or precision change. - Field
u = 1always indicates a snapshot, and continuity checks must stop.
Continuity Validation
Continuity must be checked only when u != 1.
| Condition | Action |
|---|---|
u != 1 | Validate continuity: next u should follow previous u + 1. |
u == 1 | Special snapshot (service restart / precision change). Do not perform continuity checks. |
Rules for Order Book Maintenance
First Message of connection and reconnection
After subscribing, the first message is always a snapshot.
Clients must initialize the local book with it.
Snapshot Handling
A snapshot must always replace the entire local order book:
- Clear local bids and asks.
- Rebuild from snapshot data.
- Set local
lastU = snapshot.u.
Snapshots may appear:
- after initial subscription
- when the number of changed levels > 100 (extreme market condition auto-fallback)
- after internal service restart
- after exponent / precision changes
Delta Handling
A delta applies incrementally:
- Insert/update levels with
size > 0. - Remove levels when
size == 0. - Continue continuity checks using the
ufield.
Example Push Update
Below is a real case where the connection stays healthy and messages arrive in order:
| u | Type | Notes |
|---|---|---|
| 10000 | snapshot | First message after subscription. |
| 10001 | delta | Incremental updates. Must apply changes to the existing book. |
| 10002 | delta | Normal incremental update. |
| 10003 | snapshot | Large market move (> 100 level changes). Use snapshot to replace local book. |
| 10004 | delta | Continue delta from the new snapshot. |
| 1 | snapshot | Service restarted / precision changed — reset u to 1. |
| 2 | delta | New continuity sequence. |
| 3 | delta | — |
| 4 | delta | — |
Key Differences between SBE & JSON
1. Time Precision
- JSON: timestamps typically in milliseconds (ms).
- SBE: all timestamps (
ts,cts) are in microseconds (µs) for higher precision and better sequencing accuracy.
2. Payload Size & Efficiency
- SBE: binary-packed and fixed-width where possible → very compact.
- A typical 50×2 depth update is around 1.7 KB per frame.
- JSON: verbose text format → larger payload.
- Equivalent depth update is about 2.4 KB, and can spike to 4.6 KB during volatile markets.
3. Snapshot Mode Behavior
- SBE mirrors JSON v5 behavior:
- 50-depth snapshots serve as baseline reference for delta synchronization.
Extreme Market Condition Handling
- When a delta contains more than 100 combined bid+ask updates (buy + sell), the system automatically sends a full snapshot instead of a delta.
- Ensures client books resync cleanly.
- Prevents explosion of delta packets during high churn.
- Keeps snapshot size fixed length for predictable decoding.
4. RPI (Retail Price Improvement) Exclusion
- SBE 50-level feed does not include RPI fields, only regular best bid/ask levels.
Level 50 Snapshot partially vs Level 40 full snapshot
Pros
- Delta + small partial snapshot keeps consuming less bandwidth.
- Extreme Market Handling: partial snapshot + delta is more robust.
- Supports more than 10 levels.
- Delta + partial snapshot provides continuous sync (similar to CME, Nasdaq, Binance, OKX).
Cons
- Client must handle partial snapshot + deltas to construct orderbook.
- Needs correct delta sequence alignment.
FAQ about field seq
- The
seqfield is a unified global sequence number.
It remains consistent across different depths. It is monotonically increasing, but not guaranteed to be continuous. - All messages are generated in the quote service, where both
seqanduvalues are assigned. - In the WebSocket server, messages are forwarded transparently without modification.
Therefore, for all clients connected to the same WS server, the same message has the sameseqanduvalues, regardless of the connection.
Connection
Testnet URL
wss://stream-testnet.bybits.org/v5/public-sbe/spotwss://stream-testnet.bybits.org/v5/public-sbe/linearwss://stream-testnet.bybits.org/v5/public-sbe/inverse
Mainnet URL
wss://xxxx.bybit-aws.com/v5/public-sbe/spotwss://xxxx.bybit-aws.com/v5/public-sbe/linearwss://xxxx.bybit-aws.com/v5/public-sbe/inverse
Please use your dedicated MMWS host to connect.
Data Frame Type
- SBE messages are transmitted as binary frames (
opcode = 2).
Control Messages
- Subscription, unsubscription, ping/pong, and other control messages follow the Bybit V5 API standard.
Flow
Ping / Pong (JSON control frames)
Send Ping
{"req_id": "100001", "op": "ping"}
Receive Pong
{
"success": true,
"ret_msg": "pong",
"conn_id": "xxxxx-xx",
"req_id": "",
"op": "ping"
}
Subscribe
- Topic format:
ob.50.sbe.<symbol> - Example:
BTCUSDT,ETHUSDT
Subscribe request
{"op": "subscribe", "args": ["ob.50.sbe.BTCUSDT"]}
Subscription confirmation
{
"success": true,
"ret_msg": "",
"conn_id": "d30fdpbboasp1pjbe7r0",
"req_id": "xxx",
"op": "subscribe"
}
SBE XML Template (L50 OB)
<?xml version="1.0" encoding="UTF-8"?>
<sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
xmlns:mbx="https://bybit-exchange.github.io/docs/v5/intro"
package="quote.sbe"
id="1"
version="0"
semanticVersion="1.0.0"
description="Bybit market data streams SBE message schema"
byteOrder="littleEndian"
headerType="messageHeader">
<types>
<composite name="messageHeader" description="Template ID and length of message root">
<type name="blockLength" primitiveType="uint16"/>
<type name="templateId" primitiveType="uint16"/>
<type name="schemaId" primitiveType="uint16"/>
<type name="version" primitiveType="uint16"/>
</composite>
<composite name="varString8" description="Variable length UTF-8 string.">
<type name="length" primitiveType="uint8"/>
<type name="varData" length="0" primitiveType="uint8" semanticType="String" characterEncoding="UTF-8"/>
</composite>
<composite name="groupSize16Encoding" description="Repeating group dimensions.">
<type name="blockLength" primitiveType="uint16"/>
<type name="numInGroup" primitiveType="uint16"/>
</composite>
<!-- NEW: package type enum -->
<enum name="pkgTypeEnum" encodingType="uint8">
<validValue name="SNAPSHOT">0</validValue>
<validValue name="DELTA">1</validValue>
</enum>
</types>
<!-- Stream event for "ob.rpi.1.sbe.<symbol>" channel -->
<sbe:message name="BestOBRpiEvent" id="20000">
<field id="1" name="ts" type="int64" description="The timestamp in microseconds that the system generates the data"/>
<field id="2" name="seq" type="int64" description="Cross sequence ID"/>
<field id="3" name="cts" type="int64" description="The timestamp in microseconds from the matching engine when this orderbook data is produced."/>
<field id="4" name="u" type="int64" description="Update Id"/>
<field id="5" name="askNormalPrice" type="int64" mbx:exponent="priceExponent" description="Mantissa for the best ask normal price"/>
<field id="6" name="askNormalSize" type="int64" mbx:exponent="sizeExponent" description="Mantissa for the best ask normal size"/>
<field id="7" name="askRpiPrice" type="int64" mbx:exponent="priceExponent" description="Mantissa for the best ask rpi price"/>
<field id="8" name="askRpiSize" type="int64" mbx:exponent="sizeExponent" description="Mantissa for the best ask rpi size"/>
<field id="9" name="bidNormalPrice" type="int64" mbx:exponent="priceExponent" description="Mantissa for the best bid normal price"/>
<field id="10" name="bidNormalSize" type="int64" mbx:exponent="sizeExponent" description="Mantissa for the best bid normal size"/>
<field id="11" name="bidRpiPrice" type="int64" mbx:exponent="priceExponent" description="Mantissa for the best bid rpi price"/>
<field id="12" name="bidRpiSize" type="int64" mbx:exponent="sizeExponent" description="Mantissa for the best bid rpi size"/>
<field id="13" name="priceExponent" type="int8" description="Price exponent for decimal point positioning"/>
<field id="14" name="sizeExponent" type="int8" description="Size exponent for decimal point positioning"/>
<data id="55" name="symbol" type="varString8"/>
</sbe:message>
<!-- Stream event for "ob.50.sbe.<symbol>" channel -->
<sbe:message name="OBL50Event" id="20001">
<field id="1" name="ts" type="int64" description="The timestamp in microseconds that the system generates the data"/>
<field id="2" name="seq" type="int64" description="Cross sequence ID"/>
<field id="3" name="cts" type="int64" description="The timestamp in microseconds from the matching engine when this orderbook data is produced."/>
<field id="4" name="u" type="int64" description="Update Id"/>
<field id="5" name="priceExponent" type="int8" description="Price exponent for decimal point positioning"/>
<field id="6" name="sizeExponent" type="int8" description="Size exponent for decimal point positioning"/>
<!-- NEW: package type -->
<field id="7" name="pkgType" type="pkgTypeEnum" description="Package type: 0 = SNAPSHOT (full book), 1 = DELTA (partial update)"/>
<group id="40" name="asks" dimensionType="groupSize16Encoding" description="Sell side order book updates">
<field id="1" name="price" type="int64" description="Price mantissa"/>
<field id="2" name="size" type="int64" description="Size mantissa"/>
</group>
<group id="41" name="bids" dimensionType="groupSize16Encoding" description="Buy side order book updates">
<field id="1" name="price" type="int64" description="Price mantissa"/>
<field id="2" name="size" type="int64" description="Size mantissa"/>
</group>
<data id="55" name="symbol" type="varString8"/>
</sbe:message>
</sbe:messageSchema>
SBE Level 50 – Field Reference
Message: OBL50Event (id = 20001)
| Field Name | ID | SBE Type | Unit / Format | Notes |
|---|---|---|---|---|
| ts | 1 | int64 | µs | System generation time at push side (dispatcher). |
| seq | 2 | int64 | integer | Cross-sequence id (monotonic per feed; not guaranteed continuous). |
| cts | 3 | int64 | µs | Matching-engine creation time of this OB snapshot or delta; used for latency measurements. |
| u | 4 | int64 | integer | Update id (monotonic per symbol). Useful to check continuity. |
| priceExponent | 5 | int8 | exponent | Decimal places for price. Display price = mantissa × 10^priceExponent. |
| sizeExponent | 6 | int8 | exponent | Decimal places for size. Display size = mantissa × 10^sizeExponent. |
| pkgType | 7 | uint8 (pkgTypeEnum) | integer | Package type (0 = snapshot, 1 = delta). |
| asks | 40 | group(groupSize16Encoding) | — | Sell side updates (up to 50 levels). |
| bids | 41 | group(groupSize16Encoding) | — | Buy side updates (up to 50 levels). |
| symbol | 55 | varString8 | UTF-8 | 1-byte length + bytes, e.g., 0x07 "BTCUSDT". |
Enum: pkgTypeEnum (for pkgType)
| Name | Value | Meaning |
|---|---|---|
| SNAPSHOT | 0 | Full Level-50 snapshot of the order book. Local book should be fully replaced. |
| DELTA | 1 | Incremental update on top of the last applied book. Only listed levels need updates/removal. |
Repeating group element layout (bids)
| Parent Group | Element Field | SBE Type | Unit / Format | Notes |
|---|---|---|---|---|
| asks / bids | price | int64 | mantissa | Apply priceExponent. |
| asks / bids | size | int64 | mantissa | Apply sizeExponent. |
Asks Group
Sell side orderbook updates.
| Field (id) | Type | Description |
|---|---|---|
| price (1) | int64 | Ask price mantissa. Display ask price = price × 10^priceExponent. |
| size (2) | int64 | Ask size mantissa. Display ask size = size × 10^sizeExponent. |
Bids Group
Buy side orderbook updates.
| Field (id) | Type | Description |
|---|---|---|
| price (1) | int64 | Bid price mantissa. Display bid price = price × 10^priceExponent. |
| size (2) | int64 | Bid size mantissa. Display bid size = size × 10^sizeExponent. |
Order: groups are encoded as: blockLength:uint16, numInGroup:uint16, then numInGroup elements, each exactly blockLength bytes.
In this schema each element is 16 bytes (two int64s).
Supporting composites
varString8 (variable string)
| Field | SBE Type | Notes |
|---|---|---|
| length | uint8 | Number of bytes that follow. |
| varData | uint8[length] | UTF-8 bytes. |
SBE Connection Limit
- Spot: 1500 connections limit per dedicated MMWS host.
- Futures (linear + inverse): 3000 connections limit per dedicated MMWS host.
- Once you breach the connection limit, new connections return HTTP 429.
Integration Script
Python
# sbe_ob50_client.py
import asyncio
import websockets
from typing import Tuple, List
from bybit_sbe import MessageHeader, OBL50Event # generated by SBE tool
from orderbook import OrderBook
WS_URL = "wss://stream.bybit.com/v5/market/sbe" # example
CHANNEL = "ob.50.sbe.BTCUSDT" # example symbol
def decode_obl50(buf: bytes) -> OBL50Event:
"""
Decode a single OBL50Event from binary buffer:
[messageHeader][OBL50Event]
"""
header = MessageHeader()
header.wrap(buf, 0, 0, len(buf))
if header.templateId() != 20001:
raise ValueError(f"Unexpected templateId: {header.templateId()}")
msg = OBL50Event()
# message starts right after header.encodedLength()
msg.wrapForDecode(buf, header.encodedLength(), header.blockLength(), header.version())
return msg
def to_real(value: int, exponent: int) -> float:
# mantissa * 10^exponent
return value * (10 ** exponent)
def extract_levels(msg: OBL50Event) -> Tuple[List[Tuple[float, float]], List[Tuple[float, float]]]:
px_exp = msg.priceExponent()
sz_exp = msg.sizeExponent()
asks = []
bids = []
for ask in msg.asks():
p = to_real(ask.price(), px_exp)
s = to_real(ask.size(), sz_exp)
asks.append((p, s))
for bid in msg.bids():
p = to_real(bid.price(), px_exp)
s = to_real(bid.size(), sz_exp)
bids.append((p, s))
return asks, bids
async def handle_ob50_stream():
book = OrderBook()
async with websockets.connect(WS_URL) as ws:
# Subscribe – assuming JSON subscription wrapper for SBE binary
sub_msg = {
"op": "subscribe",
"args": [CHANNEL],
}
import json
await ws.send(json.dumps(sub_msg))
while True:
raw = await ws.recv()
# Some WS setups send binary frames for SBE
if isinstance(raw, str):
# ignore non-SBE control frames, pongs etc
continue
msg = decode_obl50(raw)
u = msg.u()
pkg_type = msg.pkgType() # 0 = SNAPSHOT, 1 = DELTA
asks, bids = extract_levels(msg)
# continuity rule:
# if u == 1 => reset sequence
if u == 1:
# special snapshot (service restart / precision change)
book.bids.snapshot_from(bids)
book.asks.snapshot_from(asks)
book.last_u = 1
print(f"[RESET SNAPSHOT] u={u}, seq={msg.seq()}, symbol={msg.symbol()}")
continue
# for u != 1 => optional continuity check
if book.last_u != 0 and u != book.last_u + 1:
print(
f"[WARN] u jump detected lastU={book.last_u}, newU={u} – resync recommended"
)
# you might resubscribe or request a recovery snapshot here
if pkg_type == 0: # SNAPSHOT
book.bids.snapshot_from(bids)
book.asks.snapshot_from(asks)
else: # DELTA
for p, s in asks:
book.asks.apply_level(p, s)
for p, s in bids:
book.bids.apply_level(p, s)
book.last_u = u
# Example: print top of book
best_bid = max(book.bids.levels.keys()) if book.bids.levels else None
best_ask = min(book.asks.levels.keys()) if book.asks.levels else None
print(f"u={u} pkgType={pkg_type} bestBid={best_bid} bestAsk={best_ask}")
if __name__ == "__main__":
asyncio.run(handle_ob50_stream())
Golang
// sbe_ob50_client.go
package main
import (
"bytes"
"compress/flate"
"encoding/binary"
"encoding/json"
"fmt"
"log"
"math"
"time"
"github.com/gorilla/websocket"
"yourmodule/quote" // generated SBE package
)
const (
WSURL = "wss://stream.bybit.com/v5/market/sbe"
CHANNEL = "ob.50.sbe.BTCUSDT"
)
func toReal(mantissa int64, exponent int8) float64 {
return float64(mantissa) * math.Pow10(int(exponent))
}
func decodeOBL50(buf []byte) (*quote.OBL50Event, error) {
var hdr quote.MessageHeader
reader := bytes.NewReader(buf)
// decode messageHeader (little endian)
if err := binary.Read(reader, binary.LittleEndian, &hdr); err != nil {
return nil, fmt.Errorf("read header: %w", err)
}
if hdr.TemplateId != 20001 {
return nil, fmt.Errorf("unexpected templateId: %d", hdr.TemplateId)
}
var msg quote.OBL50Event
// many generators provide WrapForDecode; here assume we can read the fixed block then groups
if err := msg.Decode(reader, int(hdr.BlockLength), int(hdr.Version)); err != nil {
return nil, fmt.Errorf("decode OBL50: %w", err)
}
return &msg, nil
}
func main() {
book := NewOrderBook()
dialer := websocket.Dialer{
HandshakeTimeout: 10 * time.Second,
EnableCompression: false,
}
conn, _, err := dialer.Dial(WSURL, nil)
if err != nil {
log.Fatalf("dial: %v", err)
}
defer conn.Close()
// subscribe
sub := map[string]interface{}{
"op": "subscribe",
"args": []string{CHANNEL},
}
if err := conn.WriteJSON(sub); err != nil {
log.Fatalf("subscribe: %v", err)
}
for {
mt, data, err := conn.ReadMessage()
if err != nil {
log.Fatalf("read: %v", err)
}
if mt == websocket.TextMessage {
// control JSON or pong etc
var m map[string]interface{}
_ = json.Unmarshal(data, &m)
continue
}
// if server wraps SBE in per-message deflate, you may need to decompress:
if isDeflatedFrame(data) {
data, err = inflate(data)
if err != nil {
log.Printf("inflate error: %v", err)
continue
}
}
msg, err := decodeOBL50(data)
if err != nil {
log.Printf("decode error: %v", err)
continue
}
u := msg.U
pkgType := msg.PkgType // 0 snapshot, 1 delta
pxExp := msg.PriceExponent
szExp := msg.SizeExponent
// extract levels
var asks, bids [][2]float64
for _, a := range msg.Asks {
p := toReal(a.Price, pxExp)
sz := toReal(a.Size, szExp)
asks = append(asks, [2]float64{p, sz})
}
for _, b := range msg.Bids {
p := toReal(b.Price, pxExp)
sz := toReal(b.Size, szExp)
bids = append(bids, [2]float64{p, sz})
}
// continuity logic:
if u == 1 {
// service restart / precision change snapshot
book.Asks.SnapshotFrom(asks)
book.Bids.SnapshotFrom(bids)
book.LastU = 1
fmt.Printf("[RESET SNAPSHOT] u=%d seq=%d symbol=%s\n", u, msg.Seq, msg.Symbol)
continue
}
if book.LastU != 0 && u != book.LastU+1 {
log.Printf("[WARN] u jump: lastU=%d newU=%d – consider resync", book.LastU, u)
}
if pkgType == quote.PkgTypeEnum_SNAPSHOT {
book.Asks.SnapshotFrom(asks)
book.Bids.SnapshotFrom(bids)
} else {
for _, lv := range asks {
book.Asks.Apply(lv[0], lv[1])
}
for _, lv := range bids {
book.Bids.Apply(lv[0], lv[1])
}
}
book.LastU = u
bestBid := book.Bids.BestBid()
bestAsk := book.Asks.BestAsk()
fmt.Printf("u=%d pkgType=%d bestBid=%.5f bestAsk=%.5f\n", u, pkgType, bestBid, bestAsk)
}
}
// helpers (optional, depending on ws framing)
func isDeflatedFrame(data []byte) bool {
// placeholder: detect by protocol; many setups know from WS sub-protocol
return false
}
func inflate(data []byte) ([]byte, error) {
r := flate.NewReader(bytes.NewReader(data))
defer r.Close()
var out bytes.Buffer
if _, err := out.ReadFrom(r); err != nil {
return nil, err
}
return out.Bytes(), nil
}