Binance Futures WebSocket Python: Real-Time Data Guide
Learn how to connect to Binance Futures WebSocket in Python, stream live market data, and build real-time trading tools with working code examples.
Learn how to connect to Binance Futures WebSocket in Python, stream live market data, and build real-time trading tools with working code examples.
WebSocket connections are the backbone of any serious trading system. REST APIs are fine for placing orders or fetching historical data, but when you need live price feeds — millisecond-accurate order book updates, real-time trade streams, or instant liquidation alerts — WebSocket is the only practical choice. Binance Futures offers one of the most comprehensive WebSocket APIs in crypto, and Python makes it surprisingly straightforward to tap into. This guide walks you through everything from your first connection to handling reconnects and parsing real data.
REST polling has a ceiling. If you're hitting Binance's REST endpoint every second to check BTC/USDT price, you're burning rate limits and still getting stale data. Binance enforces strict IP-level rate limits — exceed them and you get temporarily banned. WebSocket flips this model: you open one persistent connection, subscribe to streams, and Binance pushes updates to you the moment they happen. For futures traders specifically, this matters enormously. A 200ms delay in spotting a large liquidation or an aggressive bid can be the difference between a good entry and chasing a move that's already done.
Platforms like Bybit and OKX offer similar WebSocket infrastructure, but Binance Futures remains the highest-volume venue for perpetual contracts, which means tighter spreads, deeper order books, and more meaningful market microstructure data flowing through those streams.
You need two core libraries: websockets for the connection layer and asyncio for non-blocking I/O. Optionally, install python-binance if you want a higher-level wrapper, but for raw WebSocket work the minimal setup gives you more control.
pip install websockets asyncio python-dotenv
For public market data streams — price tickers, trade streams, order book depth — you don't need API keys at all. Authentication is only required when subscribing to user-specific streams like account balance updates or order fills. Store your keys in a .env file and never hardcode them.
# .env file
BINANCE_API_KEY=your_api_key_here
BINANCE_SECRET_KEY=your_secret_key_here
The Binance Futures WebSocket base URL is wss://fstream.binance.com. Each stream has a specific path you append to that base. The mark price stream for BTCUSDT perpetual, for example, is /ws/btcusdt@markPrice. Here's a minimal working example that connects and prints mark price updates:
import asyncio
import json
import websockets
BASE_URL = "wss://fstream.binance.com"
async def stream_mark_price(symbol: str):
stream_path = f"/ws/{symbol.lower()}@markPrice"
url = BASE_URL + stream_path
async with websockets.connect(url) as ws:
print(f"Connected to {url}")
while True:
try:
message = await ws.recv()
data = json.loads(message)
mark_price = float(data["p"])
funding_rate = float(data["r"])
print(f"{symbol} Mark Price: {mark_price:.2f} | Funding: {funding_rate:.6f}")
except websockets.ConnectionClosed:
print("Connection closed, reconnecting...")
break
if __name__ == "__main__":
asyncio.run(stream_mark_price("BTCUSDT"))
Binance Futures WebSocket connections time out after 24 hours. Your production code must handle reconnection automatically — a one-time connection will silently die overnight.
Mark price is useful, but order book depth and aggregated trade streams are where the real signal lives. The @depth20@100ms stream gives you the top 20 bids and asks updated every 100 milliseconds. The @aggTrade stream pushes every aggregated trade as it happens, including whether the buyer or seller was the market aggressor — a key input for order flow analysis.
import asyncio
import json
import websockets
BASE_URL = "wss://fstream.binance.com"
async def stream_order_book_and_trades(symbol: str):
# Combine multiple streams using the combined stream endpoint
streams = [
f"{symbol.lower()}@depth20@100ms",
f"{symbol.lower()}@aggTrade"
]
combined_path = "/stream?streams=" + "/".join(streams)
url = BASE_URL + combined_path
async with websockets.connect(url) as ws:
print(f"Subscribed to {len(streams)} streams for {symbol}")
while True:
try:
message = await ws.recv()
envelope = json.loads(message)
stream_name = envelope["stream"]
data = envelope["data"]
if "depth" in stream_name:
best_bid = float(data["b"][0][0]) # price
best_bid_qty = float(data["b"][0][1]) # quantity
best_ask = float(data["a"][0][0])
best_ask_qty = float(data["a"][0][1])
spread = best_ask - best_bid
print(f"[Book] Bid: {best_bid} ({best_bid_qty}) | Ask: {best_ask} ({best_ask_qty}) | Spread: {spread:.2f}")
elif "aggTrade" in stream_name:
price = float(data["p"])
qty = float(data["q"])
is_buyer_maker = data["m"] # True = sell aggressor
side = "SELL" if is_buyer_maker else "BUY"
print(f"[Trade] {side} {qty} @ {price}")
except websockets.ConnectionClosed as e:
print(f"Connection closed: {e}")
break
except json.JSONDecodeError:
continue
if __name__ == "__main__":
asyncio.run(stream_order_book_and_trades("BTCUSDT"))
The combined stream endpoint is one of Binance's most useful features — a single WebSocket connection can carry up to 1024 streams simultaneously, dramatically reducing connection overhead compared to opening a separate socket per stream.
To receive account-level events — order fills, position updates, balance changes — you need a user data stream. The flow is different from public streams: you first call a REST endpoint to get a listenKey, then connect to that key's WebSocket URL. The listenKey expires every 60 minutes unless you send a keepalive ping.
import asyncio
import json
import time
import hmac
import hashlib
import httpx
import websockets
from dotenv import load_dotenv
import os
load_dotenv()
API_KEY = os.getenv("BINANCE_API_KEY")
SECRET_KEY = os.getenv("BINANCE_SECRET_KEY")
REST_BASE = "https://fapi.binance.com"
WS_BASE = "wss://fstream.binance.com"
def get_listen_key() -> str:
headers = {"X-MBX-APIKEY": API_KEY}
resp = httpx.post(f"{REST_BASE}/fapi/v1/listenKey", headers=headers)
resp.raise_for_status()
return resp.json()["listenKey"]
def keepalive_listen_key(listen_key: str):
headers = {"X-MBX-APIKEY": API_KEY}
httpx.put(
f"{REST_BASE}/fapi/v1/listenKey",
headers=headers,
params={"listenKey": listen_key}
)
async def periodic_keepalive(listen_key: str):
while True:
await asyncio.sleep(30 * 60) # every 30 minutes
keepalive_listen_key(listen_key)
print("ListenKey renewed")
async def stream_user_data():
listen_key = get_listen_key()
print(f"Got listenKey: {listen_key[:16]}...")
url = f"{WS_BASE}/ws/{listen_key}"
# Run keepalive in background
asyncio.create_task(periodic_keepalive(listen_key))
async with websockets.connect(url) as ws:
print("User data stream connected")
while True:
try:
message = await ws.recv()
event = json.loads(message)
event_type = event.get("e")
if event_type == "ORDER_TRADE_UPDATE":
order = event["o"]
print(f"Order update: {order['s']} {order['S']} {order['q']} @ {order['p']} | Status: {order['X']}")
elif event_type == "ACCOUNT_UPDATE":
balances = event["a"]["B"]
for b in balances:
if float(b["wb"]) > 0:
print(f"Balance: {b['a']} = {b['wb']}")
except websockets.ConnectionClosed:
print("User stream disconnected — refreshing listenKey")
listen_key = get_listen_key()
break
if __name__ == "__main__":
asyncio.run(stream_user_data())
Never log your listenKey to shared systems. It grants read access to your account event stream without requiring your API secret — treat it like a session token.
The examples above work great for learning, but production trading systems need robust reconnection logic. Networks drop. Binance occasionally restarts stream servers. Your bot needs to survive all of it without manual intervention. Here's a battle-tested reconnect wrapper:
import asyncio
import json
import websockets
import logging
from typing import Callable, Awaitable
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("ws_client")
async def resilient_stream(
url: str,
handler: Callable[[dict], Awaitable[None]],
max_retries: int = 10,
base_backoff: float = 1.0
):
retries = 0
while retries < max_retries:
try:
async with websockets.connect(
url,
ping_interval=20,
ping_timeout=10,
close_timeout=5
) as ws:
log.info(f"Connected: {url}")
retries = 0 # reset on successful connection
async for raw in ws:
try:
data = json.loads(raw)
await handler(data)
except json.JSONDecodeError:
log.warning("Received non-JSON message, skipping")
except Exception as e:
log.error(f"Handler error: {e}")
except (websockets.ConnectionClosed, OSError, asyncio.TimeoutError) as e:
retries += 1
wait = min(base_backoff * (2 ** retries), 60) # exponential backoff, max 60s
log.warning(f"Connection lost ({e}). Retry {retries}/{max_retries} in {wait:.1f}s")
await asyncio.sleep(wait)
log.error("Max retries reached. Giving up.")
# Usage example
async def my_handler(data: dict):
price = float(data.get("p", 0))
if price > 0:
print(f"Price update: {price}")
async def main():
url = "wss://fstream.binance.com/ws/btcusdt@markPrice"
await resilient_stream(url, my_handler)
if __name__ == "__main__":
asyncio.run(main())
Exponential backoff is critical here. If Binance has a brief outage and hundreds of bots simultaneously hammer reconnections at fixed 1-second intervals, they all hit rate limits together. Spreading reconnects out protects both your access and the exchange's infrastructure.
This pattern applies equally well when building similar systems on Bybit or OKX — both use WebSocket-based streaming with comparable reconnection requirements. Gate.io and KuCoin also offer futures WebSocket APIs, though their message formats differ enough that you'll want separate handler logic per exchange.
If you're looking for pre-processed signals rather than raw streams, VoiceOfChain aggregates real-time market data across major futures venues and surfaces actionable alerts — useful as a complement to your own WebSocket pipeline when you want cross-exchange confirmation before acting.
| Stream | Path | Update Speed | Auth Required |
|---|---|---|---|
| Mark Price | @markPrice | 1s or 3s | No |
| Aggregated Trades | @aggTrade | Real-time | No |
| Order Book Depth | @depth{N}@{ms} | 100ms / 250ms / 500ms | No |
| Candlestick (Kline) | @kline_{interval} | Per close/update | No |
| User Order Updates | listenKey | Real-time | Yes |
| Liquidation Orders | @forceOrder | Real-time | No |
With a working WebSocket pipeline you have the foundation for a serious range of trading tools. Real-time order flow imbalance detectors, funding rate monitors that alert when rates spike on Binance or Bybit, liquidation heatmaps, spread arbitrage monitors across Bitget and OKX — all of these start from exactly the code patterns above. The key architectural principle is separating your data ingestion layer (the WebSocket handlers) from your signal generation and execution logic. Keep them decoupled so you can swap in different data sources or trading logic without rewriting everything. A message queue like asyncio.Queue or Redis pub/sub between layers is the standard approach as systems grow. From there, integrating signals from VoiceOfChain or your own derived indicators becomes a straightforward pipeline addition rather than a monolithic rebuild.