◈   ⌘ api · Intermediate

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.

Uncle Solieditor · voc · 06.05.2026 ·views 22
◈   Contents
  1. → What Is the Bybit WebSocket Subscription Limit?
  2. → Connecting to Bybit WebSocket in Python
  3. → Scaling Beyond 10 Topics: Connection Pooling
  4. → Production-Grade Error Handling and Heartbeats
  5. → Private Streams: Authentication and Order Updates
  6. → Frequently Asked Questions
  7. → Building a Reliable Data Pipeline

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.

What Is the Bybit WebSocket Subscription Limit?

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.

Bybit V5 WebSocket Stream Limits by Channel Type
Stream TypeEndpointTopics per ConnectionNotes
Public Linearwss://stream.bybit.com/v5/public/linear10 per requestUSDT perpetuals
Public Spotwss://stream.bybit.com/v5/public/spot10 per requestSpot pairs
Public Inversewss://stream.bybit.com/v5/public/inverse10 per requestCoin-margined
Privatewss://stream.bybit.com/v5/private10 per requestOrders, 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.

Connecting to Bybit WebSocket in Python

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())

Scaling Beyond 10 Topics: Connection Pooling

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())

Production-Grade Error Handling and Heartbeats

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"]))

Private Streams: Authentication and Order Updates

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.

Frequently Asked Questions

What is the exact WebSocket subscription limit on Bybit?
Bybit allows up to 10 topics per subscribe request on V5 WebSocket connections. You can send multiple subscribe messages on the same connection to add more topics, but keeping total active subscriptions to a reasonable number per connection is advised to maintain data quality and avoid server-side throttling.
How do I subscribe to more than 10 topics on Bybit?
Use connection pooling — split your topic list into chunks of 10 and open a separate WebSocket connection for each chunk. Run all connections in parallel using asyncio.gather() in Python or Promise.all() in JavaScript. Each connection is independent and counts separately against rate limits.
Why does my Bybit WebSocket connection keep dropping?
Most connection drops are caused by missing heartbeats. Bybit requires you to send a ping message every 20 seconds — if you don't, the server silently closes the connection. Implement a background heartbeat task alongside your message loop and add exponential backoff on reconnection to handle transient network issues.
How is Bybit's WebSocket limit different from Binance's?
Binance allows up to 1024 streams per WebSocket connection, making it far more permissive for multi-symbol bots. Bybit's 10-topic-per-request limit is stricter and requires deliberate connection pooling when tracking many symbols. OKX and Bitget fall somewhere in between, though each has its own specific caps documented in their API references.
Can I use the same WebSocket connection for both public and private Bybit streams?
No. Public streams (orderbook, tickers, trades) and private streams (orders, positions, wallet) run on separate endpoints and require separate connections. Private connections require HMAC-SHA256 authentication before subscribing. Keep them independent — a failure in your market data connection won't affect your order update feed.
Does Bybit rate limit WebSocket connections by IP?
Yes. Bybit enforces connection rate limits at the IP level in addition to per-connection topic limits. Creating and tearing down connections rapidly (e.g. reconnecting hundreds of times per minute) can trigger temporary IP bans. Use exponential backoff on reconnection and maintain persistent long-lived connections rather than short polling connections.

Building a Reliable Data Pipeline

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.

◈   more on this topic
◉ basics Mastering the ccxt library documentation for crypto traders ⌂ exchanges Mastering the Binance CCXT Library for Crypto Traders ⌬ bots Best Crypto Trading Bots 2025: Profitable AI-Powered Strategies