Bybit WebSocket Subscription Limit: Complete Guide
Master Bybit's WebSocket subscription limits with real Python code, connection pooling strategies, and error handling patterns for reliable algo trading bots.
Master Bybit's WebSocket subscription limits with real Python code, connection pooling strategies, and error handling patterns for reliable algo trading bots.
If you've ever built a trading bot that watches more than a handful of symbols on Bybit, you've probably run into the WebSocket subscription limit the hard way — your connection drops, your data stops, and your bot goes blind mid-trade. The good news: this is a solved problem. The subscription limit isn't a barrier, it's just a constraint you need to design around. This guide covers exactly how Bybit's limit works, how to split subscriptions across multiple connections, and how to write production-grade reconnection logic that keeps your data flowing even when the network misbehaves.
Bybit's WebSocket API caps the number of topics you can subscribe to per individual connection. For the V5 public streams — which cover spot, linear perpetuals, and inverse contracts — you can subscribe to a maximum of 10 topics per connection per subscription request. Private streams (orders, positions, wallet updates) have their own separate limits. This is not unusual in the industry: Binance limits WebSocket streams to 1024 per connection, OKX caps subscriptions similarly, and Bitget enforces per-connection topic limits on its market data feeds. The difference is that Bybit's limit is more conservative on the lower end, which matters when you're tracking dozens of symbols simultaneously.
| Stream Type | Endpoint | Topics per Connection | Notes |
|---|---|---|---|
| Public Linear | wss://stream.bybit.com/v5/public/linear | 10 per request | USDT perpetuals |
| Public Spot | wss://stream.bybit.com/v5/public/spot | 10 per request | Spot pairs |
| Public Inverse | wss://stream.bybit.com/v5/public/inverse | 10 per request | Coin-margined |
| Private | wss://stream.bybit.com/v5/private | 10 per request | Orders, positions, wallet |
The 10-topic limit applies per subscription request, not per connection lifetime. You can send multiple subscribe messages on the same connection to add more topics incrementally — but keep total active subscriptions reasonable per connection to avoid dropped data and latency spikes.
Before building anything complex, get a single working connection right. The example below establishes a connection to Bybit's public linear stream, subscribes to orderbook and ticker topics for three symbols, and processes incoming messages. This is your baseline — everything more complex builds on this pattern.
import asyncio
import json
import websockets
WS_URL = "wss://stream.bybit.com/v5/public/linear"
async def connect_bybit():
async with websockets.connect(WS_URL) as ws:
# Subscribe to up to 10 topics per message
subscribe_msg = {
"op": "subscribe",
"args": [
"orderbook.1.BTCUSDT",
"orderbook.1.ETHUSDT",
"orderbook.1.SOLUSDT",
"tickers.BTCUSDT",
"tickers.ETHUSDT"
]
}
await ws.send(json.dumps(subscribe_msg))
print("Subscribed to 5 topics on one connection")
async for raw_message in ws:
msg = json.loads(raw_message)
# Bybit sends a confirmation on successful subscribe
if msg.get("op") == "subscribe":
print(f"Subscribe ack: {msg}")
continue
topic = msg.get("topic", "")
if topic.startswith("orderbook"):
data = msg["data"]
print(f"[Orderbook] {data['s']} | Bids: {len(data.get('b', []))} | Asks: {len(data.get('a', []))}")
elif topic.startswith("tickers"):
data = msg["data"]
print(f"[Ticker] {data['symbol']} | Last: {data['lastPrice']} | 24h Change: {data['price24hPcnt']}")
asyncio.run(connect_bybit())
Tracking 12 symbols means 24 orderbook topics alone if you want L1 and L2 depth. The answer is connection pooling — split your topic list into chunks of 10 and run a separate WebSocket connection for each chunk in parallel. This is how professional trading desks and platforms like VoiceOfChain handle real-time data across hundreds of instruments simultaneously. Each connection is independent, so a hiccup on one doesn't affect the others. The implementation is straightforward with Python's asyncio.
import asyncio
import json
import websockets
WS_URL = "wss://stream.bybit.com/v5/public/linear"
MAX_TOPICS_PER_CONNECTION = 10
class BybitWSPool:
def __init__(self, topics: list[str]):
# Chunk topics into groups of 10
self.chunks = [
topics[i:i + MAX_TOPICS_PER_CONNECTION]
for i in range(0, len(topics), MAX_TOPICS_PER_CONNECTION)
]
print(f"Pool: {len(topics)} topics across {len(self.chunks)} connections")
async def start(self):
tasks = [self._run_connection(chunk, idx) for idx, chunk in enumerate(self.chunks)]
await asyncio.gather(*tasks)
async def _run_connection(self, topics: list[str], conn_id: int):
while True: # auto-reconnect loop
try:
async with websockets.connect(WS_URL) as ws:
await ws.send(json.dumps({"op": "subscribe", "args": topics}))
print(f"[Conn {conn_id}] Subscribed: {topics}")
async for raw in ws:
msg = json.loads(raw)
if "topic" in msg:
await self.on_message(conn_id, msg)
except websockets.exceptions.ConnectionClosed as e:
print(f"[Conn {conn_id}] Closed ({e}), reconnecting in 3s...")
await asyncio.sleep(3)
async def on_message(self, conn_id: int, msg: dict):
topic = msg["topic"]
data = msg.get("data", {})
# Route to your strategy here
print(f"[Conn {conn_id}][{topic}] received")
# Track 25 symbols across 3 connections automatically
symbols = ["BTC", "ETH", "SOL", "BNB", "XRP", "ADA", "DOGE", "AVAX", "DOT", "LINK",
"MATIC", "UNI", "ATOM", "LTC", "APT", "ARB", "OP", "SUI", "INJ", "TIA",
"WLD", "FTM", "NEAR", "SAND", "MANA"]
topics = [f"tickers.{s}USDT" for s in symbols]
pool = BybitWSPool(topics)
asyncio.run(pool.start())
Raw connections without heartbeats will silently die. Bybit's WebSocket server expects a ping every 20 seconds; if it doesn't hear from you, it will terminate the connection without warning. Worse, the `websockets` library won't always surface this as an exception — your bot might sit there waiting for messages that will never arrive. The solution is a dedicated heartbeat task running alongside your message loop, combined with exponential backoff on reconnection. Platforms like OKX and Gate.io have the same requirement, so this pattern is portable.
import asyncio
import json
import websockets
import time
WS_URL = "wss://stream.bybit.com/v5/public/linear"
PING_INTERVAL = 20 # seconds — Bybit requires ping every 20s
async def send_heartbeat(ws: websockets.WebSocketClientProtocol):
"""Send ping every 20 seconds to keep connection alive."""
while True:
await asyncio.sleep(PING_INTERVAL)
try:
await ws.send(json.dumps({"op": "ping"}))
except websockets.exceptions.ConnectionClosed:
break
async def connect_with_retry(topics: list[str], max_retries: int = 10):
retries = 0
backoff = 1
while retries < max_retries:
try:
async with websockets.connect(WS_URL) as ws:
retries = 0 # reset on successful connection
backoff = 1
# Subscribe
await ws.send(json.dumps({"op": "subscribe", "args": topics}))
# Start heartbeat in background
heartbeat_task = asyncio.create_task(send_heartbeat(ws))
try:
async for raw in ws:
msg = json.loads(raw)
if msg.get("op") == "pong":
continue # heartbeat ack, ignore
if msg.get("op") == "subscribe" and msg.get("success"):
print(f"Subscribed successfully at {int(time.time())}")
continue
# Process real data
await handle_market_data(msg)
finally:
heartbeat_task.cancel()
except websockets.exceptions.ConnectionClosed as e:
retries += 1
print(f"Connection closed (code={e.code}). Retry {retries}/{max_retries} in {backoff}s")
await asyncio.sleep(backoff)
backoff = min(backoff * 2, 60) # exponential backoff, cap at 60s
except Exception as e:
retries += 1
print(f"Unexpected error: {type(e).__name__}: {e}")
await asyncio.sleep(backoff)
backoff = min(backoff * 2, 60)
raise RuntimeError(f"Failed to maintain connection after {max_retries} retries")
async def handle_market_data(msg: dict):
topic = msg.get("topic", "")
data = msg.get("data", {})
if topic.startswith("orderbook"):
symbol = data.get("s")
best_bid = data.get("b", [["N/A"]])[0][0]
best_ask = data.get("a", [["N/A"]])[0][0]
print(f"[{symbol}] Bid: {best_bid} | Ask: {best_ask}")
elif topic.startswith("tickers"):
print(f"[Ticker] {data.get('symbol')} @ {data.get('lastPrice')}")
# Run
asyncio.run(connect_with_retry(["orderbook.1.BTCUSDT", "tickers.BTCUSDT"]))
Watching market data is only half the picture. For order fills, position changes, and wallet updates, you need Bybit's private WebSocket stream. Authentication uses an HMAC-SHA256 signature — the same approach used by Binance and KuCoin for their private streams. You need your API key and secret, a timestamp, and a signature computed from them. The private stream runs on a separate endpoint and counts as a different connection, so it doesn't eat into your public subscription budget.
import asyncio
import json
import time
import hmac
import hashlib
import websockets
WS_PRIVATE_URL = "wss://stream.bybit.com/v5/private"
def generate_signature(api_secret: str, expires: int) -> str:
"""HMAC-SHA256 signature for Bybit auth."""
message = f"GET/realtime{expires}"
return hmac.new(
api_secret.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256
).hexdigest()
async def connect_private_stream(api_key: str, api_secret: str):
async with websockets.connect(WS_PRIVATE_URL) as ws:
# Authenticate
expires = int((time.time() + 10) * 1000) # 10s in the future
signature = generate_signature(api_secret, expires)
auth_msg = {
"op": "auth",
"args": [api_key, expires, signature]
}
await ws.send(json.dumps(auth_msg))
# Wait for auth confirmation
auth_response = json.loads(await ws.recv())
if not auth_response.get("success"):
raise PermissionError(f"Auth failed: {auth_response}")
print("Private stream authenticated")
# Subscribe to private topics
await ws.send(json.dumps({
"op": "subscribe",
"args": ["order", "position", "wallet"]
}))
async for raw in ws:
msg = json.loads(raw)
topic = msg.get("topic", "")
if topic == "order":
for order in msg.get("data", []):
print(f"[Order] {order['symbol']} {order['side']} {order['qty']} @ {order['price']} | Status: {order['orderStatus']}")
elif topic == "position":
for pos in msg.get("data", []):
print(f"[Position] {pos['symbol']} size={pos['size']} PnL={pos['unrealisedPnl']}")
elif topic == "wallet":
for coin in msg.get("data", [{}])[0].get("coin", []):
print(f"[Wallet] {coin['coin']}: available={coin['availableToWithdraw']}")
# Replace with your actual keys
# asyncio.run(connect_private_stream("YOUR_API_KEY", "YOUR_API_SECRET"))
VoiceOfChain uses real-time WebSocket connections exactly like these to deliver trading signals the moment market conditions shift — no polling delay, no stale prices. If you want pre-built signal logic instead of raw data, it's worth checking alongside your own bot infrastructure.
The Bybit WebSocket subscription limit is a constraint, not a ceiling on your ambition. With connection pooling, you can track hundreds of symbols across a handful of connections with no meaningful overhead. The real complexity isn't the limit itself — it's building the reconnection logic, heartbeat management, and message routing that makes the whole system reliable when network conditions are imperfect, as they always are in production. Start with a single working connection, add the heartbeat, then layer in the pool. Test each layer before moving to the next. The patterns in this guide will transfer directly to other exchanges too: Binance, OKX, KuCoin, and Gate.io all use WebSocket architectures with similar subscribe-and-listen patterns, and the reconnection logic is nearly identical across all of them. Once you have clean real-time data flowing, you can focus on what actually matters — the signal logic that turns raw market data into trading decisions.