Handling Bybit API Errors and Systemic Rate-Limits in Python

A beginner-friendly developer's guide to building resilient algorithmic trading bots in Python, mastering Bybit V5 HTTP error codes, preventing WebSocket disconnects, and managing rate limits effortlessly.

Stop letting unexpected network glitches, HTTP 429 rate limits, and silent WebSocket drops jeopardize your trading capital. Learn how to implement institutional-grade error handling and keep your Python bots running continuously in live market conditions.

Introduction: The True Cost of Unhandled API Failures

When beginners start building crypto trading bots in Python, they often spend 90% of their time fine-tuning entry indicators and MACD signals. However, in live market conditions on high-performance exchanges like Bybit, system stability matters just as much as strategy design. If your Python script crashes when Bybit experiences a temporary spike in traffic, your open positions are left completely unmonitored.

Every network interaction over the internet is inherently vulnerable to interruptions. A sudden surge in market volatility can cause server-side queue delays, resulting in HTTP 502 Bad Gateway or 504 Gateway Timeout errors. Simultaneously, dispatching trade requests inside fast loops can breach Bybit's endpoint quotas, triggering retCode 10006 (Too Many Visits) exceptions that lock your bot out right when you need to exit a position.

For an algorithmic trader, an unhandled exception is not just a coding bug—it is a financial risk. A missed stop-loss command due to a rate-limit block or an orphaned limit order caused by an unnoticed WebSocket drop can result in severe slippage or liquidation. Beginners often use basic blanket try-except Exception blocks, which silently hide critical errors instead of solving them.

To build a reliable automated trading engine, you need a proactive error-handling architecture. This comprehensive guide covers the full Bybit V5 API error framework, explaining how HTTP status codes compare to JSON retCodes, how token bucket rate limiting works, and how to write Python code that recovers automatically from network drops.

1. Understanding Bybit V5 API Error Architecture

The Bybit V5 API consolidates Spot, USDT Perpetual, USDC Futures, and Options trading under a single unified architecture. While this unifies data formats, error responses occur across two distinct network layers: the Transport Layer (HTTP status) and the Business Execution Layer (JSON retCode).

HTTP Status Codes vs. Bybit retCodes Explained

When your Python script sends a request to Bybit, it first passes through Cloudflare and Bybit's reverse proxies before reaching the core trading matching engine. It is essential to distinguish between these two validation points:

  • HTTP Status Codes (Transport Layer): Generated by Cloudflare edge servers or web gateways before reaching trading engines. Examples include 403 Forbidden (blocked IP or invalid authentication headers), 429 Too Many Requests (IP-level rate limit breach), and 502/504 Gateway errors (temporary exchange overload).
  • Business Error Codes (retCode Layer): Returned inside the JSON body when HTTP status is 200 OK. Bybit uses retCode = 0 to signify complete success. Any non-zero retCode (such as 10006 for rate limits or 3100115 for insufficient margin) means the request reached Bybit's matching engine but failed execution constraints.
Request Lifecycle & Error Architecture

Anatomy of a Bybit API Error Response

Layer 1: Client Application

Outbound Python Request

REST HTTP request or WebSocket payload sent to Bybit.

Headers + HMAC Signature
Layer 2: Transport & Edge

HTTP Gateway / Edge Proxy

Cloudflare nodes validate IP bandwidth, SSL, and network headers.

Errors: 403 / 429 / 502 / 504
Layer 3: Core Business Engine

Bybit Matching System

Evaluates margins, orderbook queues, API signatures, and account state.

JSON "retCode" (e.g. 0, 10006)

Essential Bybit V5 retCode Quick Reference for Beginners

Below is a breakdown of the most common retCodes beginner developers encounter, along with cause and handling guidelines:

retCodeMessageRoot CauseBeginner Handling Strategy
0OKRequest executed successfully.Process result payload safely.
10001Parameter errorInvalid symbol format or missing parameter.Log payload and fix code inputs.
10003Invalid API keyExpired or mistyped API key credentials.Stop bot immediately; update credentials.
10004Invalid signHMAC signature or timestamp drift error.Resync system clock using NTP daemon.
10006Too many visitsEndpoint rate limit capacity exceeded.Pause thread with Exponential Backoff + Jitter.
10016Server errorInternal database or matching queue delay.Short retry delay; do not spam requests.
110043Exceed max leverageLeverage requested exceeds symbol limit.Set leverage dynamically to max cap.
170213Order does not existTrying to cancel an already filled order.Update internal state; ignore error.
3100115Insufficient balanceAvailable margin cannot cover order cost.Alert trader; cancel open stale orders.

Interactive Bybit Error & Rate-Limit Simulator

Use this interactive simulator to experiment with common Bybit V5 error codes, inspect sample JSON responses, and test how exponential backoff delay changes as retry attempts increase.

Interactive Beginner Tool

Bybit V5 Error & Rate-Limit Simulator

Select an error scenario below to inspect handling logic

Rate Limit Exceeded (Too Many Visits)

Status: HTTP 200 | retCode: 10006
Core Trading Engine
What Happened

You sent too many REST API requests within a short timeframe, exceeding Bybit's endpoint weight limit.

Root Cause

High-frequency polling or sending multiple order requests in rapid un-throttled loops without backoff.

Recommended Action

Pause the current thread, trigger Exponential Backoff with jitter, and inspect X-Bapi-Limit headers.

Sample JSON Response
{
  "retCode": 10006,
  "retMsg": "Too many visits. Exceeded maximum limit for this endpoint bucket.",
  "result": {},
  "retExtInfo": {},
  "time": 1722853200000
}
Python Handling Pattern
# Catch Bybit Rate Limit (retCode 10006)
if response_data.get("retCode") == 10006:
    backoff_delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
    await asyncio.sleep(backoff_delay)

Interactive Exponential Backoff & Jitter Calculator

Retry Attempt #:3
Base Delay (sec):0.5s
Calculated Sleep2.50s

2. Building an Asynchronous HTTP Exception Handler in Python

Python developers building trading bots frequently choose asynchronous networking libraries like httpx or aiohttp to handle order placement without blocking strategy loops. The key to resilient architecture is separating network transport errors (like lost connection or timeouts) from Bybit execution errors (like signature mismatches or rate limit blocks).

Below is a clean, production-ready Python client implementation. It defines custom exception classes, signs requests automatically using HMAC-SHA256, and inspects both HTTP status codes and Bybit retCodes:

Resilient Python Asynchronous HTTP Client
import httpx
import time
import hmac
import hashlib
import logging
import json
import asyncio

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")

class BybitAPIException(Exception):
    """Custom exception raised when Bybit returns a non-zero retCode or HTTP error."""
    def __init__(self, status_code: int, ret_code: int, message: str, response_body: dict = None):
        self.status_code = status_code
        self.ret_code = ret_code
        self.message = message
        self.response_body = response_body
        super().__init__(f"Bybit Exception [HTTP {status_code}] Code {ret_code}: {message}")

class BybitTransportException(Exception):
    """Custom exception raised for network transport disruptions (timeouts, DNS failures)."""
    pass

class ResilientBybitClient:
    """Production-grade asynchronous Bybit V5 API REST Client for Python developers."""
    def __init__(self, api_key: str, api_secret: str, base_url: str = "https://api.bybit.com"):
        self.api_key = api_key
        self.api_secret = api_secret
        self.base_url = base_url
        self.client = httpx.AsyncClient(timeout=httpx.Timeout(5.0, read=10.0))

    def _generate_signature(self, timestamp: str, recv_window: str, payload: str) -> str:
        """Generates HMAC-SHA256 signature required for Bybit V5 private endpoints."""
        param_str = timestamp + self.api_key + recv_window + payload
        return hmac.new(
            self.api_secret.encode("utf-8"),
            param_str.encode("utf-8"),
            hashlib.sha256
        ).hexdigest()

    async def send_request(self, method: str, endpoint: str, params: dict = None) -> dict:
        """Dispatches HTTP request with authentication headers and robust response parsing."""
        url = f"{self.base_url}{endpoint}"
        timestamp = str(int(time.time() * 1000))
        recv_window = "5000"
        
        serialized_payload = ""
        if method == "POST" and params:
            serialized_payload = json.dumps(params)
        elif method == "GET" and params:
            serialized_payload = "&".join([f"{k}={v}" for k, v in sorted(params.items())])

        signature = self._generate_signature(timestamp, recv_window, serialized_payload)
        
        headers = {
            "X-Bapi-Api-Key": self.api_key,
            "X-Bapi-Timestamp": timestamp,
            "X-Bapi-Sign": signature,
            "X-Bapi-Recv-Window": recv_window,
            "Content-Type": "application/json"
        }

        try:
            if method == "POST":
                response = await self.client.post(url, json=params, headers=headers)
            else:
                response = await self.client.get(url, params=params, headers=headers)
                
            return self._parse_response(response)

        except httpx.TimeoutException as te:
            logging.error(f"Network timeout encountered during {method} {endpoint}: {te}")
            raise BybitTransportException("Timeout communicating with Bybit servers.") from te
        except httpx.NetworkError as ne:
            logging.error(f"Network transport disruption encountered: {ne}")
            raise BybitTransportException("Network connection lost or refused.") from ne

    def _parse_response(self, response: httpx.Response) -> dict:
        """Parses HTTP status codes and Bybit V5 retCode execution signals."""
        status_code = response.status_code
        
        try:
            data = response.json()
        except ValueError as e:
            if status_code >= 500:
                raise BybitAPIException(status_code, -500, "Severe Exchange Gateway Degradation (HTML returned).", response.text) from e
            raise BybitAPIException(status_code, -1, "Invalid JSON payload received from exchange.", response.text) from e

        ret_code = data.get("retCode")
        ret_msg = data.get("retMsg", "No error message provided.")

        # Check HTTP Gateway restrictions
        if status_code in [429, 403] and ret_code != 0:
            raise BybitAPIException(status_code, ret_code, f"IP-Level Threshold Restriction: {ret_msg}", data)

        # Check Business Execution Layer
        if ret_code != 0:
            raise BybitAPIException(status_code, ret_code, ret_msg, data)

        return data.get("result", {})

3. Strategic Rate-Limit Management & IP Weight Headers

To protect exchange infrastructure from automated spam, Bybit enforces strict rate limits. Rate limits are tracked across two tiers: Account/Endpoint limits and IP-level limits. Exceeding account endpoint limits triggers retCode 10006, while breaching IP limits results in an HTTP 429 status code or a Cloudflare IP ban.

Bybit Special Offer

Grab a $100 sign-up bonus, earn up to $30,000 in deposit bonuses, VIP status upgrades and exclusive algorithmic rewards.

Our Partner Code
BYNINJA

How Bybit's Token Bucket Allocation Works

Bybit manages endpoint quotas using a Token Bucket algorithm. Rather than granting a uniform request limit per second across all endpoints, Bybit groups endpoints into distinct functional buckets. For instance, creating an order on linear perpetual contracts has a separate bucket (typically 10 requests per second per account) compared to querying market candle data.

Inspecting Bybit Response Headers

Bybit returns real-time rate limit capacity details in the HTTP response headers after every request:

  • X-Bapi-Limit: The total request capacity assigned to this specific endpoint bucket.
  • X-Bapi-Limit-Status: The number of requests remaining in the current time window.
  • X-Bapi-Limit-Reset-Timestamp: The exact Unix millisecond timestamp when full capacity refills.

Preventing Thundering Herds with Exponential Backoff & Jitter

When a rate limit occurs, naive scripts repeatedly retry every 100ms, compounding the rate-limit lockout. The correct approach is Exponential Backoff with Random Jitter. Jitter adds small randomized time variations to retry delays, preventing multiple concurrent trading sub-processes from spamming the exchange simultaneously when a bucket resets.

Python Rate Limit Retry Decorator with Jitter
import asyncio
import random
import logging
from functools import wraps

def retry_on_rate_limit(max_retries: int = 5, base_delay: float = 0.5, max_delay: float = 10.0):
    """
    Python Decorator for handling Bybit V5 Rate Limits (retCode 10006 / HTTP 429)
    and transient HTTP gateway timeouts with Exponential Backoff + Random Jitter.
    """
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            retries = 0
            while True:
                try:
                    return await func(*args, **kwargs)
                except BybitAPIException as e:
                    # retCode 10006 represents 'Too many visits' / Endpoint Rate Limit
                    if e.ret_code == 10006 or e.status_code == 429:
                        retries += 1
                        if retries > max_retries:
                            logging.critical("Max retries breached for rate limit. Escalating exception.")
                            raise e
                        
                        # Calculate exponential backoff with full jitter
                        delay = min(max_delay, base_delay * (2 ** (retries - 1)))
                        jittered_delay = delay + random.uniform(0, 0.5 * delay)
                        
                        logging.warning(
                            f"Rate limit hit during {func.__name__}. "
                            f"Attempt {retries}/{max_retries}. Retrying in {jittered_delay:.2f}s..."
                        )
                        await asyncio.sleep(jittered_delay)
                        continue
                    
                    # If it's a structural server error (502, 503, 504), perform a fast retry
                    if e.status_code in [502, 503, 504]:
                        retries += 1
                        if retries > max_retries:
                            raise e
                        await asyncio.sleep(base_delay * retries)
                        continue
                    
                    # For all other business logic errors (e.g. Insufficient Balance 3100115), fail immediately
                    raise e
                except BybitTransportException:
                    retries += 1
                    if retries > max_retries:
                        raise
                    await asyncio.sleep(base_delay * retries)
                    continue
        return wrapper
    return decorator

4. Engineering Resilient WebSockets with Auto-Reconnection

While REST API calls are ideal for discrete order commands, real-time market data (order books, ticker streams) and private execution updates should always be received via WebSockets. WebSockets eliminate HTTP request overhead, but they require active management against silent connection drops and ISP disconnects.

Bybit WebSocket Heartbeat Protocol (Ping-Pong)

Bybit's WebSocket servers close idle connections that fail to transmit periodic heartbeat signals. To keep your socket connection healthy:

  • Heartbeat Interval: Your Python client must send {"op": "ping"} every 20 seconds.
  • Pong Response Verification: Listen for Bybit's returning {"op": "pong"} frame. If no pong arrives within 5 seconds, assume the connection is dead and trigger immediate reconnection.
Connection Lifecycle & Heartbeat Keep-Alive

Bybit WebSocket Heartbeat & Reconnection Flow

Timer Loop (20s)

Client Heartbeat Ping

Transmits {"op": "ping"} frame every 20 seconds.

Interval: 20 Seconds
5s Limit
Health Check

Pong Verification

Awaits {"op": "pong"} response within 5 seconds.

Status: Monitoring
Atomic Recovery

Auto-Reconnect Routine

Opens new WebSocket stream, authenticates, and re-subscribes instantly.

Zero Data Loss Pipeline
Asyncio WebSocket Client with Heartbeat & Auto-Reconnection
import websockets
import json
import asyncio
import logging

class ResilientBybitWS:
    """
    Production Asynchronous WebSocket Manager for Bybit V5 API.
    Handles continuous heartbeat ping-pong, atomic reconnection, and channel resubscription.
    """
    def __init__(self, uri: str = "wss://stream.bybit.com/v5/public/linear"):
        self.uri = uri
        self.ws = None
        self.is_running = False
        self.subscriptions = []

    async def connect(self):
        logging.info(f"Establishing persistent connection to Bybit WebSocket: {self.uri}")
        self.ws = await websockets.connect(self.uri)
        self.is_running = True
        # Re-subscribe to channels if recovering from a disconnect
        if self.subscriptions:
            await self._subscribe(self.subscriptions)

    async def monitor_connection(self):
        """Monitors socket state and triggers automatic reconnection on disconnection."""
        while self.is_running:
            try:
                if not self.ws or self.ws.closed:
                    logging.warning("WebSocket disruption detected! Initiating reconnection loop...")
                    await self.connect()
                await asyncio.sleep(5)
            except Exception as e:
                logging.error(f"Error during WebSocket connection health check: {e}")
                await asyncio.sleep(5)

    async def start(self, channels: list):
        self.subscriptions = channels
        await self.connect()
        
        # Concurrently fire heartbeat, read loop, and connection health monitor
        await asyncio.gather(
            self._heartbeat_loop(),
            self._read_loop(),
            self.monitor_connection()
        )

    async def _subscribe(self, channels: list):
        payload = {
            "op": "subscribe",
            "args": channels
        }
        await self.ws.send(json.dumps(payload))
        logging.info(f"Subscription payload transmitted for: {channels}")

    async def _heartbeat_loop(self):
        """Sends ping payload every 20 seconds to prevent Bybit idle socket pruning."""
        while self.is_running:
            try:
                if self.ws and not self.ws.closed:
                    await self.ws.send(json.dumps({"op": "ping"}))
                    logging.debug("WebSocket Ping frame transmitted.")
            except Exception as e:
                logging.error(f"Failed sending heartbeat ping: {e}")
            await asyncio.sleep(20)

    async def _read_loop(self):
        """Receives incoming data frames and handles ping-pong control frames."""
        while self.is_running:
            try:
                if self.ws and not self.ws.closed:
                    message = await self.ws.recv()
                    data = json.loads(message)
                    
                    # Intercept pong response
                    if data.get("op") == "pong" or data.get("ret_msg") == "pong":
                        logging.debug("WebSocket Pong frame received safely.")
                        continue
                    
                    # Pass market data payload to strategy processor
                    await self.handle_market_data(data)
            except websockets.exceptions.ConnectionClosed:
                logging.error("WebSocket connection closed by Bybit edge server.")
                await asyncio.sleep(1)
            except Exception as e:
                logging.error(f"Unexpected error processing inbound WS message: {e}")
                await asyncio.sleep(0.1)

    async def handle_market_data(self, data: dict):
        """Custom handler for processing orderbook tickers and trade execution updates."""
        if "topic" in data:
            logging.info(f"Stream Update [{data['topic']}]: Received market payload.")

5. Beginner's Guide: Setting Up Structured Error Logging

When a trading bot encounters an issue at 3:00 AM, print statements (print(e)) are insufficient for post-mortem debugging. Production trading scripts require structured dual-logging: printing clean real-time status updates to the terminal screen while writing detailed timestamps, file names, and tracebacks to an audit log file.

Below is a simple logging helper you can include at the start of your Python trading bot script:

Python Dual Console & File Logging Utility
import logging
import sys

def setup_bot_logging(log_filename: str = "bybit_trading_bot.log"):
    """
    Configures a dual-output structured logger for beginner traders.
    Outputs clean format to terminal stdout and records detailed tracebacks in file.
    """
    logger = logging.getLogger("BybitBot")
    logger.setLevel(logging.DEBUG)

    # Console Handler for real-time monitoring
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setLevel(logging.INFO)
    console_format = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s", "%H:%M:%S")
    console_handler.setFormatter(console_format)

    # File Handler for audit trail and post-mortem analysis
    file_handler = logging.FileHandler(log_filename)
    file_handler.setLevel(logging.DEBUG)
    file_format = logging.Formatter("%(asctime)s [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s")
    file_handler.setFormatter(file_format)

    logger.addHandler(console_handler)
    logger.addHandler(file_handler)
    
    return logger

Key Logging Best Practices for Beginners

  • Never log API Secret Keys: Ensure key credentials and headers are scrubbed from log files to prevent accidental leakages.
  • Log raw retMsg on non-zero retCode: Always record Bybit's exact return message alongside the retCode for quick reference.
  • Monitor log file size: Use standard RotatingFileHandler in production to prevent log files from taking over disk space.

6. Architectural Guardrails for Production Trading Engines

Building a robust trading bot involves more than catching isolated errors; it requires embedding guardrails into your architecture:

1. Maintain an In-Memory Local State Machine

Avoid querying REST API endpoints repeatedly in fast loops to check order fills or position sizes. High-frequency polling rapidly exhausts endpoint weights. Instead, mirror your active positions locally using low-latency WebSocket updates, reserving REST calls primarily for order submissions and daily sanity syncs.

2. Implement a Circuit Breaker Pattern

In corporate trading systems, circuit breakers automatically halt order placement if a threshold of consecutive API errors (e.g. five straight 10016 server errors) is reached. Pausing execution for a designated cooldown period (e.g. 60 seconds) protects your account equity during severe exchange maintenance.

3. Maintain Precise NTP Clock Synchronization

Bybit rejects signed REST requests if your system clock drifts more than 5,000 milliseconds from Bybit server time, returning retCode 10004. Always run a background clock synchronization daemon (such as chrony or NTP) on your server.

Frequently Asked Questions (FAQ)

Q1: Why am I receiving HTTP 403 Forbidden errors when my API key is completely valid?

An HTTP 403 error is generated at the Cloudflare edge network, not Bybit's trading engine. It occurs if your server IP is flagged for sending malformed HTTP payloads, or if your VPS provider shares an IP subnet with known malicious traffic. Solution: Host your bot on a reputable cloud provider (AWS, GCP, DigitalOcean) or assign a dedicated static IP.

Q2: How do I know how much rate limit capacity my bot has left in real time?

Inspect the incoming HTTP response headers after every call. Check X-Bapi-Limit-Status. If this counter drops below 20% of X-Bapi-Limit, instruct your Python thread manager to introduce a short sleep delay until X-Bapi-Limit-Reset-Timestamp passes.

Q3: Are public market data endpoints subject to the same rate limits as order execution?

No. Bybit imposes strict REST limits on public ticker and orderbook polling to prevent bandwidth congestion. Traders are strongly encouraged to stream real-time market data over free, un-throttled Public WebSockets, reserving private REST endpoints for order management.

Q4: What should my bot do if an HTTP timeout occurs immediately after dispatching an order?

This is known as an 'in-flight timeout'. The request may have reached Bybit and filled, but the confirmation response was lost in transit. Retrying blindly risks placing duplicate orders. Instead, catch the timeout exception, query your local WebSocket position state or call order sync via a custom orderLinkId, and verify order status before retrying.

High-Value Technical Concepts for Modern Algorithmic Trading

When engineering automated trading infrastructure in Python, core optimization topics include Bybit V5 REST API pagination handlers, Python asyncio websockets connection pool management, handling high-volatility exchange drops, and implementing thread-safe request token buckets. Integrating these structural concepts ensures your trading engine remains performant and protected against network anomalies during volatile market events.

Build Trading Systems That Outlast Market Turbulence

Ready to elevate your quantitative execution to a completely new standard of performance?