◈   ⌘ api · Intermediate

OKX WebSocket Message Rate: Complete Dev Guide

Master OKX WebSocket message rate limits, connection handling, and subscription management to build reliable crypto trading bots without disconnections.

Uncle Solieditor · voc · 19.05.2026 ·views 6
◈   Contents
  1. → How OKX WebSocket Rate Limits Actually Work
  2. → Setting Up an OKX WebSocket Connection with Rate Control
  3. → Subscribing to Channels Without Hitting the Rate Limit
  4. → Handling Disconnections and Reconnection Logic
  5. → Order Rate Limits on the Private WebSocket
  6. → Monitoring and Debugging Rate Limit Issues in Production
  7. → Frequently Asked Questions
  8. → Building Reliable OKX WebSocket Integrations

If your OKX trading bot randomly disconnects or stops receiving data mid-session, message rate limits are almost certainly the culprit. OKX enforces strict controls on how many messages a client can send per connection, and violating those limits doesn't just slow you down — it terminates your session entirely. Understanding exactly how these limits work is the difference between a bot that runs 24/7 and one that silently dies at 3 AM.

How OKX WebSocket Rate Limits Actually Work

OKX operates two distinct WebSocket endpoints: a public one for market data and a private one for account and order operations. Each comes with its own rate limit profile. For the public WebSocket, you're allowed to send up to 3 subscription messages per second. The private WebSocket is tighter — you can place up to 60 orders per second across all instruments combined, but subscription operations follow the same 3 messages/second ceiling.

The key thing most developers miss: the rate limit applies to outbound messages from your client, not to inbound data you receive. OKX can push hundreds of ticker updates per second to you without issue. The restriction is on how fast you're sending subscribe, unsubscribe, and order requests back to their servers. Exceed the threshold and you'll receive an error code before being disconnected.

OKX uses error code 60014 ('Requests too frequent') when you breach the message rate. Always log this code explicitly — a silent disconnect is harder to debug than a logged rate limit error.
OKX WebSocket Rate Limits by Endpoint
EndpointMax Subscribe MessagesMax Order OperationsConnection Limit
Public WS (wss://ws.okx.com:8443/ws/v5/public)3 msg/secN/A3 per IP
Private WS (wss://ws.okx.com:8443/ws/v5/private)3 msg/sec60 orders/sec3 per account
Business WS (wss://ws.okx.com:8443/ws/v5/business)3 msg/secN/A3 per IP

Setting Up an OKX WebSocket Connection with Rate Control

Before you can worry about rate limits, you need a stable connection. OKX requires HMAC-SHA256 authentication for private channels. Here's a production-ready connection setup in Python using the websockets library, with rate limiting baked in from the start.

import asyncio
import websockets
import json
import hmac
import hashlib
import base64
import time
from collections import deque

API_KEY = "your_api_key"
SECRET_KEY = "your_secret_key"
PASSPHRASE = "your_passphrase"

PUBLIC_WS_URL = "wss://ws.okx.com:8443/ws/v5/public"
PRIVATE_WS_URL = "wss://ws.okx.com:8443/ws/v5/private"

def generate_signature(timestamp: str, secret: str) -> str:
    message = timestamp + "GET" + "/users/self/verify"
    mac = hmac.new(secret.encode(), message.encode(), hashlib.sha256)
    return base64.b64encode(mac.digest()).decode()

class RateLimiter:
    """Token bucket: max 3 messages per second"""
    def __init__(self, rate: int = 3, per: float = 1.0):
        self.rate = rate
        self.per = per
        self.timestamps = deque()

    async def acquire(self):
        now = time.monotonic()
        # Remove timestamps older than the window
        while self.timestamps and now - self.timestamps[0] >= self.per:
            self.timestamps.popleft()
        if len(self.timestamps) >= self.rate:
            sleep_time = self.per - (now - self.timestamps[0])
            await asyncio.sleep(sleep_time)
        self.timestamps.append(time.monotonic())

async def authenticate(ws, api_key: str, secret: str, passphrase: str):
    timestamp = str(int(time.time()))
    sig = generate_signature(timestamp, secret)
    auth_msg = {
        "op": "login",
        "args": [{
            "apiKey": api_key,
            "passphrase": passphrase,
            "timestamp": timestamp,
            "sign": sig
        }]
    }
    await ws.send(json.dumps(auth_msg))
    response = await ws.recv()
    data = json.loads(response)
    if data.get("event") != "login" or data.get("code") != "0":
        raise ConnectionError(f"Auth failed: {data}")
    print("Authenticated successfully")

Subscribing to Channels Without Hitting the Rate Limit

The most common mistake when building OKX bots is firing off all subscriptions at once during startup. If you're subscribing to 20 trading pairs, sending all 20 subscribe messages in a tight loop will breach the 3 messages/second limit immediately. The fix is a queued subscription system that spaces out your requests.

async def subscribe_with_rate_limit(ws, channels: list, limiter: RateLimiter):
    """
    Subscribe to multiple channels respecting OKX's 3 msg/sec limit.
    OKX allows batching multiple args in a single subscribe message,
    so group up to 10 channels per message to maximize efficiency.
    """
    BATCH_SIZE = 10  # OKX accepts multiple args per subscribe op

    for i in range(0, len(channels), BATCH_SIZE):
        batch = channels[i:i + BATCH_SIZE]
        sub_msg = {
            "op": "subscribe",
            "args": batch
        }
        await limiter.acquire()  # Wait if we're at the limit
        await ws.send(json.dumps(sub_msg))
        print(f"Subscribed to batch {i // BATCH_SIZE + 1}: {[c['channel'] for c in batch]}")

async def main():
    limiter = RateLimiter(rate=3, per=1.0)

    channels = [
        {"channel": "tickers", "instId": "BTC-USDT"},
        {"channel": "tickers", "instId": "ETH-USDT"},
        {"channel": "tickers", "instId": "SOL-USDT"},
        {"channel": "books5", "instId": "BTC-USDT"},
        {"channel": "books5", "instId": "ETH-USDT"},
        {"channel": "trades", "instId": "BTC-USDT"},
    ]

    async with websockets.connect(PUBLIC_WS_URL, ping_interval=20) as ws:
        await subscribe_with_rate_limit(ws, channels, limiter)

        # Main message loop
        async for message in ws:
            data = json.loads(message)

            # Handle subscription confirmations
            if data.get("event") == "subscribe":
                print(f"Confirmed: {data.get('arg')}")
                continue

            # Handle rate limit errors
            if data.get("event") == "error" and data.get("code") == "60014":
                print("Rate limit hit — backing off 1 second")
                await asyncio.sleep(1)
                continue

            # Process actual market data
            if "data" in data:
                process_market_data(data)

def process_market_data(data: dict):
    channel = data.get("arg", {}).get("channel", "unknown")
    inst_id = data.get("arg", {}).get("instId", "unknown")
    payload = data.get("data", [])
    print(f"[{channel}] {inst_id}: {payload[0] if payload else 'empty'}") 

asyncio.run(main())

Notice the batching strategy above — OKX allows you to include multiple channel args in a single subscribe message. This means you can subscribe to 10 channels with one message instead of ten, which is a 10x improvement in subscription efficiency within the same rate limit envelope. Compare this to Binance, which also supports batch subscriptions, but Bybit requires individual subscribe calls per channel — making OKX's batching a real advantage for wide market surveillance bots.

Handling Disconnections and Reconnection Logic

A rate limit violation results in a forced disconnect. But disconnections also happen for other reasons: network hiccups, OKX server maintenance, or the 30-second ping timeout if you're not sending heartbeats. Robust bots handle all these cases with exponential backoff and automatic re-subscription.

import random

MAX_RECONNECT_ATTEMPTS = 10
BASE_BACKOFF = 1.0  # seconds

async def connect_with_retry(channels: list, private: bool = False):
    url = PRIVATE_WS_URL if private else PUBLIC_WS_URL
    limiter = RateLimiter(rate=3, per=1.0)
    attempt = 0

    while attempt < MAX_RECONNECT_ATTEMPTS:
        try:
            async with websockets.connect(
                url,
                ping_interval=20,   # Send WS ping every 20s
                ping_timeout=10,    # Disconnect if no pong in 10s
                close_timeout=5
            ) as ws:
                print(f"Connected (attempt {attempt + 1})")
                attempt = 0  # Reset on successful connection

                if private:
                    await authenticate(ws, API_KEY, SECRET_KEY, PASSPHRASE)

                await subscribe_with_rate_limit(ws, channels, limiter)

                # OKX also requires application-level pings every 30s
                async def send_heartbeat():
                    while True:
                        await asyncio.sleep(25)
                        await ws.send("ping")

                heartbeat_task = asyncio.create_task(send_heartbeat())

                try:
                    async for message in ws:
                        if message == "pong":
                            continue  # Heartbeat response, ignore
                        data = json.loads(message)
                        if data.get("event") == "error":
                            code = data.get("code")
                            print(f"WS error {code}: {data.get('msg')}")
                            if code == "60014":  # Rate limit
                                await asyncio.sleep(1)
                        elif "data" in data:
                            process_market_data(data)
                finally:
                    heartbeat_task.cancel()

        except (websockets.exceptions.ConnectionClosed,
                websockets.exceptions.WebSocketException,
                OSError) as e:
            attempt += 1
            backoff = BASE_BACKOFF * (2 ** attempt) + random.uniform(0, 1)
            backoff = min(backoff, 60)  # Cap at 60 seconds
            print(f"Disconnected: {e}. Reconnecting in {backoff:.1f}s (attempt {attempt})")
            await asyncio.sleep(backoff)

    print("Max reconnection attempts reached. Manual intervention required.")
OKX requires both WebSocket-level pings (handled by the library) AND application-level 'ping' string messages every 25-30 seconds. If you only rely on the library's ping_interval, you'll still get disconnected by OKX's own 30-second inactivity timeout. Always send both.

Order Rate Limits on the Private WebSocket

For high-frequency strategies, the private WebSocket order rate limit matters far more than subscriptions. OKX allows 60 order operations per second on the private WS, which sounds generous until you're running a market-making bot on multiple pairs simultaneously. Each place-order, cancel-order, and amend-order operation counts against this limit. Batch order endpoints help significantly — a single batch message can contain up to 20 orders and still counts as one operation toward the rate limit.

Compare this to the REST API alternative: OKX REST order endpoints cap at 60 requests/second too, but each HTTP round trip adds 50-150ms of latency. WebSocket orders typically settle in 5-20ms. For scalping strategies on OKX, Bybit, or Bitget, this latency difference is the entire edge. Platforms like VoiceOfChain that aggregate real-time signals rely on sub-100ms data pipelines precisely because WebSocket is the only transport fast enough to make the signals actionable.

WebSocket vs REST: OKX Order Operations Comparison
MetricWebSocket OrdersREST API Orders
Latency5–20 ms50–150 ms
Rate Limit60 ops/sec60 req/sec
Batch SupportUp to 20 orders/msgUp to 20 orders/req
Connection OverheadPersistentPer-request TCP/TLS
Best ForHFT, market makingLow-frequency, simple bots

Monitoring and Debugging Rate Limit Issues in Production

Silent rate limit disconnections are the hardest bugs to chase in live trading systems. The bot appears to be running, the process is alive, but it stopped receiving data 20 minutes ago. Here's how to build observability into your OKX WebSocket client so you catch problems before they cost you.

Professional trading infrastructure on exchanges like OKX and Binance typically runs multiple WebSocket connections in parallel — one per asset class or strategy — with each connection staying well under the message rate ceiling. If you need to monitor 50 pairs, split them across 5 connections of 10 pairs each rather than cramming everything into one overloaded session. This also provides natural fault isolation: if one connection drops, the other four keep running.

Frequently Asked Questions

What is the OKX WebSocket message rate limit?
OKX allows a maximum of 3 outbound subscription messages per second on both public and private WebSocket endpoints. For order operations on the private WebSocket, the limit is 60 operations per second. Exceeding either limit triggers error code 60014 and a forced disconnect.
How do I avoid getting disconnected from OKX WebSocket?
Implement a rate limiter (token bucket or sliding window) before every ws.send() call. Also send application-level 'ping' strings every 25 seconds — OKX disconnects clients that are inactive for 30 seconds, separate from the WebSocket protocol-level ping. Reconnect logic with exponential backoff handles inevitable disconnections gracefully.
Can I subscribe to multiple channels in one OKX WebSocket message?
Yes, and you should. OKX accepts a single subscribe message with an args array containing up to multiple channel objects. This lets you subscribe to 10 channels with one message instead of ten, which is much more efficient within the 3 messages/second limit.
What is error code 60014 on OKX WebSocket?
Error code 60014 means 'Requests too frequent' — you've exceeded the message rate limit. The connection will be terminated after this error. Back off for at least 1 second before reconnecting and implementing proper rate limiting to prevent recurrence.
Is OKX WebSocket faster than the REST API for placing orders?
Significantly faster. WebSocket orders on OKX typically settle in 5-20ms versus 50-150ms for REST API calls, because the connection is already established and there's no per-request TCP/TLS handshake. For market-making or scalping strategies, this latency difference is material.
How many simultaneous WebSocket connections does OKX allow?
OKX allows up to 3 simultaneous WebSocket connections per IP for public endpoints, and up to 3 per account for private endpoints. For large-scale monitoring, spread subscriptions across multiple connections and consider using multiple IP addresses if needed.

Building Reliable OKX WebSocket Integrations

OKX's WebSocket rate limits are strict but entirely manageable with the right architecture. The core pattern is simple: batch your subscriptions, rate-limit every outbound message, send heartbeats at both the protocol and application level, and build reconnection logic that resubscribes automatically. With these fundamentals in place, your OKX WebSocket connection becomes a stable foundation rather than a fragile dependency.

For traders running signal-driven strategies, the speed advantage of WebSocket over REST is only realized if the connection stays stable. Tools like VoiceOfChain are built on exactly this kind of persistent, fault-tolerant WebSocket infrastructure — ingesting real-time order flow from OKX, Binance, Bybit, and other major exchanges without interruption. The same principles that keep those pipelines running apply to your own trading bots: respect the limits, handle failures gracefully, and the data will flow reliably.

◈   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