REST API vs WebSocket: The Crypto Trader's Latency Guide
REST API vs WebSocket is one of the most impactful protocol choices for crypto traders. Learn the latency differences, practical use cases, and code examples for Binance, Bybit, and OKX.
REST API vs WebSocket is one of the most impactful protocol choices for crypto traders. Learn the latency differences, practical use cases, and code examples for Binance, Bybit, and OKX.
Speed kills in crypto trading — or rather, the lack of it does. Whether you're running a scalping bot on Binance, monitoring liquidation cascades on Bybit, or triangulating arbitrage across OKX and Coinbase, the data protocol you choose determines how fresh your market view actually is. REST and WebSocket are both valid tools, but choosing the wrong one for the wrong job will bleed latency — and in crypto, milliseconds translate directly into missed fills, bad entry prices, and lost edge.
REST (Representational State Transfer) is the classic request-response model. Your application sends an HTTP request to an exchange endpoint, the server processes it, responds with JSON, and the connection closes. Simple, stateless, and universally supported across every exchange that exists. For every piece of data you want, you initiate a new round-trip. Binance's REST API at api.binance.com serves endpoints like /api/v3/ticker/price for spot prices, /api/v3/depth for order book snapshots, and /api/v3/klines for candlestick history. Each call is independent — great for flexibility, but the connection overhead accumulates fast. Even from a co-located server adjacent to Binance's matching engine, a REST call typically runs 5–50ms. From a home broadband connection, budget 50–300ms per call. If your strategy needs fresh price data every 100ms, you're spending most of that window waiting for network round-trips instead of computing or deciding. Polling REST for streaming data is the most common performance mistake new algo traders make.
REST polling at 1-second intervals means your price data is on average 500ms stale. A WebSocket connection eliminates this lag entirely — you receive updates the moment the exchange publishes them, not when you remembered to ask.
REST isn't wrong — it's a tool with specific use cases. Order placement, account balance queries, historical OHLCV downloads, withdrawals, and one-time data lookups are all excellent REST use cases. The request-response model maps cleanly to these operations: you ask once, you get your answer, you move on. Problems only appear when developers try to use REST for real-time data streaming — which it was never designed to handle efficiently.
import requests
import hmac
import hashlib
import time
API_KEY = "your_api_key_here"
API_SECRET = "your_api_secret_here"
BASE_URL = "https://api.binance.com"
def get_price(symbol: str) -> float:
"""Public endpoint — no authentication needed."""
response = requests.get(
f"{BASE_URL}/api/v3/ticker/price",
params={"symbol": symbol},
timeout=5
)
response.raise_for_status()
return float(response.json()["price"])
def get_account_balance() -> list:
"""Private endpoint — requires HMAC-SHA256 signature."""
timestamp = int(time.time() * 1000)
query = f"timestamp={timestamp}"
signature = hmac.new(
API_SECRET.encode(), query.encode(), hashlib.sha256
).hexdigest()
response = requests.get(
f"{BASE_URL}/api/v3/account",
headers={"X-MBX-APIKEY": API_KEY},
params={"timestamp": timestamp, "signature": signature},
timeout=5
)
response.raise_for_status()
return [b for b in response.json()["balances"] if float(b["free"]) > 0]
try:
price = get_price("BTCUSDT")
print(f"BTC price: ${price:,.2f}")
balances = get_account_balance()
for b in balances:
print(f"{b['asset']}: {b['free']}")
except requests.exceptions.Timeout:
print("Request timed out — check your connection or Binance status")
except requests.exceptions.HTTPError as e:
print(f"API error {e.response.status_code}: {e.response.text}")
WebSockets flip the model entirely. Instead of you asking repeatedly, you establish one persistent TCP connection and the exchange pushes data to you the moment something changes. No polling overhead, no repeated TLS handshakes, no wasted bandwidth asking 'anything new?' when the answer is often 'no.' Bybit's WebSocket API at stream.bybit.com and OKX at ws.okx.com both push order book updates continuously. You subscribe to a stream once — say, BTCUSDT depth20 — and receive every change to the top 20 price levels without asking for them. The connection stays open and data flows in both directions as needed. The latency difference is significant. A REST call to fetch Binance's order book might take 30–100ms from a typical server. A WebSocket update for the same data arrives within 1–5ms of the matching engine publishing it. For a scalper who needs to know immediately when a large sell wall disappears, that 95ms structural advantage is the entire edge. WebSocket connections also dramatically reduce API rate limit pressure. Instead of 10 REST calls per second consuming your weight limits, one WebSocket subscription delivers unlimited updates on that symbol for as long as the connection stays open.
import asyncio
import json
import websockets
BINANCE_WS = "wss://stream.binance.com:9443/ws"
async def stream_orderbook(symbol: str):
"""Real-time order book updates — no polling, exchange pushes each change."""
stream = f"{symbol.lower()}@depth5@100ms"
url = f"{BINANCE_WS}/{stream}"
async with websockets.connect(url, ping_interval=20) as ws:
print(f"Connected: {symbol} order book stream")
async for raw_message in ws:
data = json.loads(raw_message)
best_bid = data["bids"][0] # [price, quantity]
best_ask = data["asks"][0]
spread = float(best_ask[0]) - float(best_bid[0])
spread_bps = (spread / float(best_ask[0])) * 10000
print(
f"Bid: {best_bid[0]:>12} | "
f"Ask: {best_ask[0]:>12} | "
f"Spread: {spread_bps:.2f} bps"
)
async def main():
try:
await stream_orderbook("BTCUSDT")
except websockets.exceptions.ConnectionClosed as e:
print(f"Connection closed: {e.code} — reconnecting...")
except Exception as e:
print(f"Stream error: {e}")
asyncio.run(main())
Raw latency numbers depend on server location, network path, and exchange load — but the relative differences between REST and WebSocket are consistent across environments. Here's what traders consistently observe when benchmarking against major exchanges:
| Metric | REST API | WebSocket |
|---|---|---|
| Connection model | New TCP+TLS per request | One-time handshake, persistent |
| Data freshness (home) | 50–300ms per poll | 1–15ms push delay |
| Data freshness (co-located) | 5–50ms per poll | < 5ms push delay |
| API rate limit impact | High (counted per request) | Minimal (per subscription) |
| Order placement | 5–50ms (correct use) | N/A — use REST |
| Best suited for | Orders, auth, history | Prices, books, live trades |
| Reconnection cost | Zero (stateless) | Must implement backoff logic |
The co-located server numbers deserve attention. If you're serious about competitive arbitrage or HFT, running your bot in an AWS region adjacent to the exchange datacenter — ap-northeast-1 (Tokyo) for Binance and Bybit, eu-west-1 for some Coinbase Advanced Trade operations — cuts REST latency to single-digit milliseconds. But even at the same co-location, a persistent WebSocket connection beats REST polling for streaming data by an order of magnitude. The two approaches are not competing for the same job.
Professional trading systems don't choose one protocol over the other — they use both for what each does best. The standard pattern: WebSocket for all market data ingestion (prices, order books, trade feeds, liquidation events), REST for all discrete actions (order placement, account management, balance checks). This is exactly the architecture that real-time signal platforms like VoiceOfChain operate on. A persistent WebSocket feed processes live market data across dozens of pairs simultaneously, while REST handles the discrete authenticated operations that follow from analysis. Signal latency in this model is driven by your strategy's computation time, not network overhead — because data is already sitting in memory the instant the exchange publishes it. Gate.io and KuCoin also expose both interfaces, following the same pattern: public WebSocket streams for market data, authenticated REST or private WebSocket for order management. The hybrid approach scales cleanly across any exchange that follows this convention.
import asyncio
import json
import hmac
import hashlib
import time
import requests
import websockets
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class MarketState:
bid: Optional[float] = None
ask: Optional[float] = None
class HybridTrader:
def __init__(self, api_key: str, api_secret: str):
self.api_key = api_key
self.api_secret = api_secret
self.state = MarketState()
self.base_url = "https://api.binance.com"
async def run_price_stream(self, symbol: str):
"""WebSocket handles market data — runs continuously in background."""
url = f"wss://stream.binance.com:9443/ws/{symbol.lower()}@bookTicker"
async with websockets.connect(url) as ws:
async for msg in ws:
data = json.loads(msg)
self.state.bid = float(data["b"])
self.state.ask = float(data["a"])
def place_order(self, symbol: str, side: str, qty: float) -> dict:
"""REST handles order execution — happens once per trade signal."""
ts = int(time.time() * 1000)
params = {
"symbol": symbol, "side": side,
"type": "MARKET", "quantity": qty,
"timestamp": ts
}
query = "&".join(f"{k}={v}" for k, v in params.items())
sig = hmac.new(
self.api_secret.encode(), query.encode(), hashlib.sha256
).hexdigest()
params["signature"] = sig
resp = requests.post(
f"{self.base_url}/api/v3/order",
headers={"X-MBX-APIKEY": self.api_key},
params=params,
timeout=3
)
resp.raise_for_status()
return resp.json()
def check_signal(self) -> Optional[str]:
"""Strategy logic — data is always fresh because WebSocket is live."""
if self.state.bid and self.state.ask:
spread_bps = ((self.state.ask - self.state.bid) / self.state.ask) * 10000
if spread_bps < 0.5: # Tight spread — good entry condition
return "BUY"
return None
async def main():
trader = HybridTrader("your_key", "your_secret")
stream = asyncio.create_task(trader.run_price_stream("BTCUSDT"))
await asyncio.sleep(2) # Let stream populate initial state
signal = trader.check_signal()
if signal:
result = trader.place_order("BTCUSDT", signal, 0.001)
print(f"Order placed: {result['orderId']}")
stream.cancel()
asyncio.run(main())
The REST vs WebSocket decision in crypto trading is not a matter of preference — it's an architectural choice with measurable performance consequences. Use WebSocket for everything that needs to be real-time: price feeds, order book updates, live trade streams, liquidation data. Use REST for everything discrete: order placement, account management, historical data pulls, authentication flows. Platforms like VoiceOfChain are built on exactly this hybrid model — persistent WebSocket connections for live signal generation, authenticated REST calls for the actions that follow. Build your stack the same way, co-locate if latency is your edge, and the protocol layer stops being a bottleneck.