Key takeawaysMarket Streams push public events without Spot request weight, while the Spot WebSocket API handles requests and private subscriptions and shares request and account order limits with REST.
- Market Streams push public events without Spot request weight, while the Spot WebSocket API handles requests and private subscriptions and shares request and account order limits with REST.
- The 1,024 ceiling counts subscribed stream names, not symbols; one all-market stream can represent updates for many trading pairs.
- Spot connections expire after 24 hours, and
serverShutdownis a separate shutdown signal; production consumers need proactive handover, backoff, jitter, queues, and gap detection. - Spot and USDⓈ-M do not use one universal accounting model: USDⓈ-M WebSocket IP weight is separate from REST, while its
ORDERSlimit is shared by UID. - Monitor connection age, last-event age, processing lag, queue depth, sequence gaps, close reasons, and planned rotations instead of treating an open socket as proof of health.
This summary was created with AI.
Two mechanisms with similar names
Market streams are the push service for public market data. You subscribe to named streams in the URL or with JSON control messages, and Binance sends trades, tickers, klines, or depth updates as they happen. Receiving those events does not consume Spot REQUEST_WEIGHT.
The control channel still matters. A stream client can send SUBSCRIBE, UNSUBSCRIBE, LIST_SUBSCRIPTIONS, SET_PROPERTY, and GET_PROPERTY messages. They do not become weighted REST-style API calls, but they count toward the stream connection's limit of 5 client-to-server messages per second together with Ping and Pong frames.
The Spot WebSocket API is primarily request-response. A client sends an id, a method, and parameters, and receives a response tied to the same id. It can also carry subscribed private account events: after userDataStream.subscribe or its signed variant, events such as account and order updates arrive asynchronously with a subscriptionId, not as replies to a request.
That makes the current distinction more precise:
| Property | Market Streams | Spot WebSocket API |
|---|---|---|
| Main purpose | Continuous public market events | Requests, trading, account queries, and private events |
| Message model | Server-pushed events plus subscription control | Request-response plus subscribed account events |
| Spot request weight | Incoming market events use none | Methods consume the same Spot REQUEST_WEIGHT pool as REST |
| Connection cost | No Spot request weight for opening the stream | Opening a connection costs 2 request weight |
| Order limits | Not applicable | ORDERS is maintained per account and shared across keys and APIs |
| Rate-limit visibility | No rateLimits array on market events |
Responses include rateLimits by default |
| Connection attempts | 300 per 5 minutes per IP | 300 per 5 minutes per IP |
| Maximum lifetime | 24 hours | 24 hours |
The WebSocket API documentation therefore needs to be read separately from the market-stream reference even though both use the same transport. They share some connection rules, but they solve different jobs and do not have one interchangeable limit model.
A persistent WebSocket can reduce HTTP framing and simplify multiplexing many outstanding requests through one bidirectional connection. It does not mean that REST always opens a new TCP and TLS session for each call: a normal REST client can reuse connections through keep-alive and pooling. Choose the interface for its workflow and operational model, not because one transport is assumed to reconnect on every request.
Choosing between REST and streams
The practical rule is still useful: if the application would poll a changing market value, subscribe to its stream. Use REST or the WebSocket API when you need a snapshot, a historical range, an account query, or a trading action with a direct response.
The REST and WebSocket documentation divides those jobs deliberately. Streams are not a database: they deliver changes after the connection is established. A local order book still needs the documented snapshot-plus-buffer procedure, and a consumer that detects a sequence gap must discard or rebuild state instead of pretending that the connection remained complete.
What the 1,024-stream ceiling counts
The limit applies to stream names subscribed on one connection, not directly to the number of symbols represented in incoming payloads.
For example:
btcusdt@tradeis one stream for one symbol;- subscribing to both
btcusdt@tradeandbtcusdt@depthuses two streams; !miniTicker@arris one stream even though each event may contain updates for many symbols;- all-market rolling-window streams can also represent many symbols under one stream name.
A wide symbol universe therefore does not automatically require one stream per symbol. First choose the narrowest stream types that provide the data the application needs, then count the actual subscription names. Split connections when that count approaches 1,024 or when processing volume, fault isolation, and recovery time justify a smaller shard.
Connection lifetime, heartbeat, and shutdown
For current Spot streams and the Spot WebSocket API:
- each connection is valid for at most 24 hours;
- the server sends a Ping frame every 20 seconds;
- the client must return a Pong carrying the same payload within one minute;
- unsolicited Pong frames do not replace the required reply;
serverShutdownis sent when the server is about to shut down.
The 24-hour expiry and serverShutdown are separate cases. Do not assume that every normal 24-hour rotation will first produce that event. The client must handle a timed rotation, an announced server shutdown, an ordinary close frame, and an abrupt network loss.
The current market-stream limit is 5 client-to-server messages per second, including Ping, Pong, and JSON control messages. A consumer that repeatedly exceeds it can be disconnected, and repeated violations can lead to an IP ban. Subscription changes should therefore be batched and rate-limited instead of sent in a burst.
A reconnecting Python consumer
The following WebSocket client in Python separates socket reading from event processing with a bounded queue. It resets reconnect backoff only after the connection has remained stable, uses full jitter, and rotates before the 24-hour limit.
from __future__ import annotations
import asyncio
import json
import random
import time
from typing import Any
import websockets
URL = (
"wss://stream.binance.com:9443/stream"
"?streams=btcusdt@trade/ethusdt@trade"
)
ROTATE_AFTER_SECONDS = 23 * 60 * 60 + 45 * 60
STABLE_AFTER_SECONDS = 60
MAX_BACKOFF_SECONDS = 60
EVENT_QUEUE_SIZE = 10_000
def is_server_shutdown(message: dict[str, Any]) -> bool:
if message.get("stream") == "!serverShutdown":
return True
payload = message.get("data", message)
return isinstance(payload, dict) and payload.get("e") == "serverShutdown"
async def process_events(queue: asyncio.Queue[dict[str, Any]]) -> None:
while True:
message = await queue.get()
try:
payload = message.get("data", message)
print(payload.get("e"), payload.get("s"), payload.get("p"))
finally:
queue.task_done()
async def consume_one_connection(
queue: asyncio.Queue[dict[str, Any]],
) -> tuple[str, float]:
connected_at = time.monotonic()
# The server sends Ping frames. The library automatically replies with Pong.
async with websockets.connect(
URL,
ping_interval=None,
close_timeout=5,
max_queue=1_024,
) as websocket:
while True:
age = time.monotonic() - connected_at
remaining = ROTATE_AFTER_SECONDS - age
if remaining <= 0:
return "planned_rotation", age
try:
raw = await asyncio.wait_for(
websocket.recv(),
timeout=min(30.0, remaining),
)
except asyncio.TimeoutError:
continue
message = json.loads(raw)
if is_server_shutdown(message):
return "server_shutdown", time.monotonic() - connected_at
try:
queue.put_nowait(message)
except asyncio.QueueFull as exc:
raise RuntimeError(
"event queue is full; reconnect and resynchronize state"
) from exc
async def consume_forever() -> None:
queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue(
maxsize=EVENT_QUEUE_SIZE
)
worker = asyncio.create_task(process_events(queue))
backoff = 1.0
try:
while True:
reason = "connection_error"
connection_age = 0.0
try:
reason, connection_age = await consume_one_connection(queue)
except (websockets.WebSocketException, OSError, RuntimeError) as exc:
print("disconnected:", type(exc).__name__, exc)
if connection_age >= STABLE_AFTER_SECONDS:
backoff = 1.0
if reason in {"planned_rotation", "server_shutdown"}:
delay = random.uniform(0.0, 1.0)
else:
delay = random.uniform(0.0, backoff)
backoff = min(backoff * 2, MAX_BACKOFF_SECONDS)
print(
f"reconnecting: reason={reason}, "
f"age={connection_age:.1f}s, delay={delay:.1f}s"
)
await asyncio.sleep(delay)
finally:
worker.cancel()
await asyncio.gather(worker, return_exceptions=True)
asyncio.run(consume_forever())
The code is an operational skeleton, not a complete trading-data engine. A queue overflow is treated as a state-integrity failure instead of silently dropping events. For a depth stream, reconnecting is not enough: discard the local book and repeat the snapshot-and-buffer synchronization. For ticker-style state, a design may choose to replace older queued values with the newest one, but that policy must be explicit.
Frequent reconnects do not identify one cause by themselves. Record the close code and reason, connection age, last-event age, queue depth, processing lag, subscription count, and egress route. Short connections can come from heartbeat failures, message-rate violations, server shutdown, network or proxy timeouts, event-loop stalls, backpressure, or application exceptions.
How to rotate without a data gap
The sample above closes one connection and then opens the next, which leaves a small handover gap. A continuous production service should rotate proactively:
1. Open a replacement connection before the old one reaches 24 hours.
2. Recreate the same subscriptions on the replacement.
3. Wait until the new connection has delivered valid events.
4. Deduplicate the overlap using trade IDs, event IDs, or stream-specific sequences.
5. Atomically switch downstream consumers to the replacement.
6. Close the old connection after the overlap is reconciled.
For a local order book, the replacement must build and validate its own snapshot-plus-buffer state before the switch. Do not merge two depth streams blindly, because overlapping update IDs and a missed gap can corrupt the book.
Production metrics
At minimum, expose:
websocket_connection_age_seconds{host,shard}
websocket_reconnect_total{host,reason,close_code}
websocket_last_event_age_ms{stream}
websocket_processing_lag_ms{stream}
websocket_queue_depth{shard}
websocket_subscription_count{shard}
websocket_control_messages_per_second{shard}
websocket_gap_total{stream}
websocket_server_shutdown_total{host}
websocket_planned_rotation_total{host}
websocket_egress_ip_info{worker,ip}
Alert separately on stale data, repeated short-lived connections, queue saturation, and sequence gaps. A socket can remain open while its consumer is already unhealthy.
Streams on the futures platform
Futures uses separate hosts and separate product rules, so Spot constants should not be copied into a shared configuration without verification.
For USDⓈ-M, the futures WebSocket API currently has an important accounting difference:
- its WebSocket API
REQUEST_WEIGHTis counted per IP; - that WebSocket weight pool is shared across the USDⓈ-M WebSocket API hosts, but not with the REST IP weight pool;
ORDERSis counted per UID and is shared with REST;- opening a WebSocket API connection costs 5 weight.
That is not the same model as Spot, where WebSocket API request weight shares the Spot pool with REST. The overlap on USDⓈ-M is primarily the order-rate accounting and specified order operations, not one universal IP-weight counter.
Heartbeat and stream limits are also product-specific. Current Spot documentation uses a 20-second Ping and one-minute Pong window. The currently published Binance.US stream document is dated September 2023 and still lists a three-minute Ping with a ten-minute Pong window. That is the latest published US reference, but its age should be recorded rather than presented as a behavior newly verified in 2026.
Keep these values in configuration tied to the exact product, host, and documentation version:
product
stream_host
websocket_api_host
maximum_connection_age
server_ping_interval
pong_deadline
control_message_limit
connection_attempt_limit
request_weight_scope
order_limit_scope
One stable egress can simplify IP whitelisting, logs, and incident attribution, but a separate address for every consumer is not the first fix for reconnect storms. Start with backoff, full jitter, planned staggered rotations, bounded concurrency, and a shared connection-attempt coordinator. Additional egress addresses make sense when workloads are genuinely independent or require separate network identities.
That is the relevant role of our proxies for Binance: dedicated IPv4 addresses reserved exclusively for you for the plan term, providing predictable egress for IP whitelisting and independently operated workloads. They do not replace correct reconnect logic or Binance rate-limit handling.