WebSocket Crypto Price Streams: A Real-Time Trading Guide
WebSocket connections deliver real-time crypto prices in milliseconds — essential for algo traders. Learn to connect to Binance, Bybit, and OKX streams with working code examples.
WebSocket connections deliver real-time crypto prices in milliseconds — essential for algo traders. Learn to connect to Binance, Bybit, and OKX streams with working code examples.
Every millisecond matters in crypto. The difference between a filled order and a missed trade often comes down to how fast your price data arrives. REST APIs — where you ping a server every second and wait for a response — are a relic of a slower era. WebSocket connections flip that model: instead of you asking for data, the exchange pushes it to you the instant prices change. That's the foundation of any serious real-time trading setup, whether you're building a bot, running signals, or just watching the market tick.
If you've ever polled a REST endpoint every second to get the latest Bitcoin price, you've already felt the limitations. You're burning API rate limits, adding round-trip latency on every request, and still missing price action between each poll. On a volatile day — think a sudden 5% BTC move in three minutes — your REST-based system is always behind.
The crypto price websocket API model solves this directly. A single persistent TCP connection replaces hundreds of HTTP requests per minute. The exchange server pushes each price update as it happens — no polling, no rate limit waste, no artificial delay. On Binance, the ticker WebSocket stream fires updates every 100 milliseconds. On Bybit's V5 WebSocket, updates arrive on every trade. The difference between REST polling and WebSocket streaming can be hundreds of milliseconds per update — and in high-frequency strategies, that's the entire edge.
REST API polling at 1-second intervals = up to 1000ms latency per update. WebSocket push = typically 10-100ms. For any strategy sensitive to execution timing, this gap is not cosmetic — it's structural.
A WebSocket connection starts as a standard HTTP request with an 'Upgrade' header. The server agrees to upgrade the protocol, and from that point the connection stays open — both sides can send data freely without re-establishing the link. For crypto price feeds, you subscribe to specific channels (like a BTC/USDT ticker stream) and the exchange sends you a JSON message every time a relevant event fires.
Most exchange WebSocket APIs share a common pattern: open the connection, send a subscription message specifying the instrument and data type you want, then enter a receive loop to process incoming messages. Authentication is only required for private channels — account balances, order updates, positions. For public market data like price tickers, order book depth, and trade history, no API key is needed. This makes it trivial to connect to multiple exchanges simultaneously without worrying about credential management for data-only feeds.
Binance has one of the most developer-friendly WebSocket implementations in the industry. For the websocket bitcoin price feed, you just open a connection to a predictable URL — no subscription message required for simple ticker streams. The response delivers a full 24-hour rolling ticker: current price, best bid and ask, volume, and percent change, all arriving roughly every 100ms.
import asyncio
import websockets
import json
async def stream_binance_price(symbol="btcusdt"):
url = f"wss://stream.binance.com:9443/ws/{symbol}@ticker"
async with websockets.connect(url) as ws:
print(f"Connected to Binance WebSocket for {symbol.upper()}")
while True:
msg = await ws.recv()
data = json.loads(msg)
price = float(data['c']) # current close price
change = float(data['P']) # 24h price change percent
volume = float(data['v']) # base asset traded volume
bid = float(data['b']) # best bid price
ask = float(data['a']) # best ask price
print(
f"{symbol.upper()}: ${price:,.2f} "
f"| 24h: {change:+.2f}% "
f"| Bid: {bid:,.2f} / Ask: {ask:,.2f} "
f"| Vol: {volume:,.0f}"
)
asyncio.run(stream_binance_price("btcusdt"))
The key fields in the Binance ticker response: 'c' is last price, 'P' is 24h percent change, 'v' is base asset volume, 'b' is best bid, 'a' is best ask. Binance also supports combined streams — you can subscribe to multiple symbols in one connection using the format wss://stream.binance.com:9443/stream?streams=btcusdt@ticker/ethusdt@ticker. This matters at scale: Binance limits connections per IP, so batching symbols into combined streams keeps you well under those limits.
One of the most powerful WebSocket use cases is cross-exchange monitoring — watching Bybit and OKX at the same time to spot price divergences or confirm momentum across venues. Python's asyncio makes this clean: spin up a coroutine for each exchange and run them concurrently with asyncio.gather. Unlike Binance's URL-based approach, both Bybit and OKX require you to send a JSON subscription message after connecting.
import asyncio
import websockets
import json
async def stream_bybit(symbol="BTCUSDT"):
url = "wss://stream.bybit.com/v5/public/spot"
async with websockets.connect(url) as ws:
subscribe_msg = {
"op": "subscribe",
"args": [f"tickers.{symbol}"]
}
await ws.send(json.dumps(subscribe_msg))
while True:
msg = await ws.recv()
data = json.loads(msg)
# Skip subscription confirmation messages
if data.get("topic", "").startswith("tickers"):
ticker = data["data"]
price = float(ticker.get("lastPrice", 0))
print(f"[Bybit] {symbol}: ${price:,.2f}")
async def stream_okx(symbol="BTC-USDT"):
url = "wss://ws.okx.com:8443/ws/v5/public"
async with websockets.connect(url) as ws:
subscribe_msg = {
"op": "subscribe",
"args": [{"channel": "tickers", "instId": symbol}]
}
await ws.send(json.dumps(subscribe_msg))
while True:
msg = await ws.recv()
data = json.loads(msg)
if "data" in data:
ticker = data["data"][0]
price = float(ticker.get("last", 0))
print(f"[OKX] {symbol}: ${price:,.2f}")
async def main():
await asyncio.gather(
stream_bybit("BTCUSDT"),
stream_okx("BTC-USDT")
)
asyncio.run(main())
Notice that Bybit and OKX both use a subscription model — and their formats differ in the details. Bybit's V5 API uses a flat 'args' array with a string like 'tickers.BTCUSDT'. OKX wraps each subscription in an object with 'channel' and 'instId' keys. These small differences catch people off guard when they try to reuse the same subscription code across exchanges. The pattern of filtering out non-data messages (confirmation pings, subscription acks) before parsing is essential to avoid JSON key errors.
Always handle the subscription confirmation message before expecting data. Both Bybit and OKX send a success response after subscribing — if your parser expects ticker data immediately, it will crash on the confirmation message.
WETH (Wrapped Ether) trades on both centralized exchanges and DeFi protocols. For the weth crypto price on CEX, Binance lists it as WETHUSDT — streamable with the exact same ticker WebSocket as any other pair. WETH generally tracks ETH within a few basis points since it's redeemable 1:1, but brief deviations appear during heavy DeFi activity or when wrapped ETH demand spikes on Layer 2 bridges. Monitoring WETH alongside ETH can surface these micro-dislocations before they close.
Production WebSocket clients need reconnection logic — full stop. Connections drop due to server maintenance, network interruptions, or exchange-side restarts. Without a reconnect loop, your stream silently dies and your strategy runs on stale data without knowing it. Here's a production-grade wrapper with exponential backoff reconnection, a recv timeout, and structured logging:
import asyncio
import websockets
import json
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
logger = logging.getLogger(__name__)
async def stream_with_reconnect(symbol, url, on_price, max_retries=10):
retry_count = 0
while retry_count < max_retries:
try:
async with websockets.connect(
url,
ping_interval=20, # send WebSocket ping every 20s
ping_timeout=10, # disconnect if no pong in 10s
close_timeout=5
) as ws:
logger.info(f"Connected: {symbol}")
retry_count = 0 # reset counter on successful connect
while True:
msg = await asyncio.wait_for(ws.recv(), timeout=30)
data = json.loads(msg)
if 'c' in data: # Binance ticker format
await on_price(symbol, float(data['c']))
except websockets.exceptions.ConnectionClosed as e:
retry_count += 1
wait = min(2 ** retry_count, 60) # exponential backoff, cap at 60s
logger.warning(f"{symbol} closed ({e}). Retry {retry_count}/{max_retries} in {wait}s")
await asyncio.sleep(wait)
except asyncio.TimeoutError:
retry_count += 1
logger.warning(f"{symbol} recv timeout — reconnecting")
await asyncio.sleep(5)
except Exception as e:
retry_count += 1
logger.error(f"{symbol} error: {e}")
await asyncio.sleep(10)
logger.error(f"{symbol}: max retries exhausted")
async def handle_price(symbol, price):
logger.info(f"{symbol}: ${price:.4f}")
async def main():
weth_url = "wss://stream.binance.com:9443/ws/wethusdt@ticker"
await stream_with_reconnect("WETHUSDT", weth_url, handle_price)
asyncio.run(main())
The exponential backoff pattern — 2^retry capped at 60 seconds — prevents your reconnect loop from hammering an exchange during an outage. Most exchanges temporarily block IPs that send excessive connection attempts in a short window. The ping_interval and ping_timeout parameters let the websockets library handle low-level keepalives automatically, which is critical for connections that sit idle during low-volatility periods — NAT gateways will silently drop them otherwise.
Platforms like VoiceOfChain use exactly this kind of persistent WebSocket architecture to power real-time trading signals. Rather than fetching prices on demand, the signal engine maintains live streams from multiple exchanges simultaneously and computes signals the moment new price data arrives. That sub-second reaction time is what separates signals that lead price moves from ones that always seem to confirm them a few minutes too late.
WebSocket streaming is not an optimization — it's a prerequisite for anything serious in crypto trading. Whether you're monitoring the websocket bitcoin price across Binance and Bybit, tracking the weth crypto price alongside ETH, or aggregating feeds from OKX, Coinbase, and KuCoin into a unified signal engine, the pattern is the same: open a persistent connection, subscribe to your channels, and process data as it arrives. The code examples above give you a working foundation — add your own logic for signal generation, alerting, or order execution on top of them.
The reconnection layer is the piece most traders skip and later regret. Connections will drop. Build the backoff logic in from the start and you'll never wake up to a bot running on six-hour-old prices. For traders who'd rather consume real-time signals than build the data infrastructure from scratch, platforms like VoiceOfChain handle the WebSocket plumbing across multiple exchanges and deliver actionable alerts directly — but understanding the mechanics underneath makes you a sharper user of any signal platform.