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.
Master OKX WebSocket message rate limits, connection handling, and subscription management to build reliable crypto trading bots without disconnections.
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.
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.
| Endpoint | Max Subscribe Messages | Max Order Operations | Connection Limit |
|---|---|---|---|
| Public WS (wss://ws.okx.com:8443/ws/v5/public) | 3 msg/sec | N/A | 3 per IP |
| Private WS (wss://ws.okx.com:8443/ws/v5/private) | 3 msg/sec | 60 orders/sec | 3 per account |
| Business WS (wss://ws.okx.com:8443/ws/v5/business) | 3 msg/sec | N/A | 3 per IP |
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")
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.
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.
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.
| Metric | WebSocket Orders | REST API Orders |
|---|---|---|
| Latency | 5–20 ms | 50–150 ms |
| Rate Limit | 60 ops/sec | 60 req/sec |
| Batch Support | Up to 20 orders/msg | Up to 20 orders/req |
| Connection Overhead | Persistent | Per-request TCP/TLS |
| Best For | HFT, market making | Low-frequency, simple bots |
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.
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.