Skip to main content

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:

TypeMeaning
snapshotA full 50-level orderbook image. Must reset the local book.
deltaIncremental updates. Must apply changes to the existing book.

Rules for the u (Update ID) Field

Behavior of u

  • Field u increases monotonically for all snapshots and deltas.
  • Field u does not reset, unless there is a system reset or precision change.
  • Field u = 1 always indicates a snapshot, and continuity checks must stop.

Continuity Validation

Continuity must be checked only when u != 1.

ConditionAction
u != 1Validate continuity: next u should follow previous u + 1.
u == 1Special 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 u field.

Example Push Update

Below is a real case where the connection stays healthy and messages arrive in order:

uTypeNotes
10000snapshotFirst message after subscription.
10001deltaIncremental updates. Must apply changes to the existing book.
10002deltaNormal incremental update.
10003snapshotLarge market move (> 100 level changes). Use snapshot to replace local book.
10004deltaContinue delta from the new snapshot.
1snapshotService restarted / precision changed — reset u to 1.
2deltaNew continuity sequence.
3delta
4delta

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 seq field 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 seq and u values 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 same seq and u values, regardless of the connection.

Connection

Testnet URL

  • wss://stream-testnet.bybits.org/v5/public-sbe/spot
  • wss://stream-testnet.bybits.org/v5/public-sbe/linear
  • wss://stream-testnet.bybits.org/v5/public-sbe/inverse

Mainnet URL

  • wss://xxxx.bybit-aws.com/v5/public-sbe/spot
  • wss://xxxx.bybit-aws.com/v5/public-sbe/linear
  • wss://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 NameIDSBE TypeUnit / FormatNotes
ts1int64µsSystem generation time at push side (dispatcher).
seq2int64integerCross-sequence id (monotonic per feed; not guaranteed continuous).
cts3int64µsMatching-engine creation time of this OB snapshot or delta; used for latency measurements.
u4int64integerUpdate id (monotonic per symbol). Useful to check continuity.
priceExponent5int8exponentDecimal places for price. Display price = mantissa × 10^priceExponent.
sizeExponent6int8exponentDecimal places for size. Display size = mantissa × 10^sizeExponent.
pkgType7uint8 (pkgTypeEnum)integerPackage type (0 = snapshot, 1 = delta).
asks40group(groupSize16Encoding)Sell side updates (up to 50 levels).
bids41group(groupSize16Encoding)Buy side updates (up to 50 levels).
symbol55varString8UTF-81-byte length + bytes, e.g., 0x07 "BTCUSDT".

Enum: pkgTypeEnum (for pkgType)

NameValueMeaning
SNAPSHOT0Full Level-50 snapshot of the order book. Local book should be fully replaced.
DELTA1Incremental update on top of the last applied book. Only listed levels need updates/removal.

Repeating group element layout (bids)

Parent GroupElement FieldSBE TypeUnit / FormatNotes
asks / bidspriceint64mantissaApply priceExponent.
asks / bidssizeint64mantissaApply sizeExponent.

Asks Group

Sell side orderbook updates.

Field (id)TypeDescription
price (1)int64Ask price mantissa. Display ask price = price × 10^priceExponent.
size (2)int64Ask size mantissa. Display ask size = size × 10^sizeExponent.

Bids Group

Buy side orderbook updates.

Field (id)TypeDescription
price (1)int64Bid price mantissa. Display bid price = price × 10^priceExponent.
size (2)int64Bid 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)

FieldSBE TypeNotes
lengthuint8Number of bytes that follow.
varDatauint8[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
}