Ключевые выводыMarket Streams пушат публичные события без Spot request weight, а Spot WebSocket API обслуживает запросы и приватные подписки, разделяя с REST вес и ордерные лимиты.
- Market Streams пушат публичные события без Spot request weight, а Spot WebSocket API обслуживает запросы и приватные подписки, разделяя с REST вес и ордерные лимиты.
- Потолок 1 024 относится к именам потоков, а не к символам: один all-market stream может передавать обновления сразу по многим парам.
- Spot-соединение заканчивается через 24 часа, а
serverShutdown— отдельный сигнал; production consumer нужны плановая передача, backoff, jitter, очередь и контроль gaps. - Spot и USDⓈ-M используют разные модели: у USDⓈ-M IP weight WebSocket API отделён от REST, тогда как
ORDERSразделяется по UID. - Контролируйте возраст соединения и событий, processing lag, queue depth, sequence gaps, close reasons и плановые ротации, а не считайте открытый socket доказательством здоровья.
Краткое содержание подготовлено с помощью ИИ.
Два механизма с похожими названиями
Потоки рыночных данных — это push-сервис публичных событий. Вы подписываетесь на названия потоков через URL или управляющие JSON-сообщения, после чего Binance присылает сделки, тикеры, свечи или обновления стакана. Получение этих событий не расходует Spot REQUEST_WEIGHT.
Управляющий канал при этом существует. Клиент может отправлять SUBSCRIBE, UNSUBSCRIBE, LIST_SUBSCRIPTIONS, SET_PROPERTY и GET_PROPERTY. Они не превращаются во взвешенные REST-подобные вызовы, но входят в лимит пяти client-to-server сообщений в секунду вместе с Ping и Pong.
Spot WebSocket API работает прежде всего по модели запрос-ответ. Клиент отправляет id, имя метода и параметры, а затем получает ответ с тем же id. Но сервис умеет и приватные подписки: после userDataStream.subscribe или подписанного варианта события аккаунта и ордеров приходят асинхронно с subscriptionId, а не как ответы на конкретный запрос.
Точное сравнение выглядит так:
| Свойство | Market Streams | Spot WebSocket API |
|---|---|---|
| Основное назначение | Непрерывные публичные рыночные события | Запросы, торговля, данные аккаунта и приватные события |
| Модель сообщений | Push-события и управление подписками | Запрос-ответ плюс события аккаунта |
| Spot request weight | Входящие события его не расходуют | Методы расходуют общий с REST пул |
| Стоимость подключения | Открытие stream не расходует Spot weight | Подключение стоит 2 request weight |
| Ордерные лимиты | Не применяются | ORDERS ведётся по аккаунту и делится между ключами и API |
| Информация о лимитах | В market events нет rateLimits |
Ответы по умолчанию содержат rateLimits |
| Попытки подключения | 300 за 5 минут на IP | 300 за 5 минут на IP |
| Максимальная жизнь | 24 часа | 24 часа |
Документацию сервиса запрос-ответ нужно читать отдельно от справочника потоков, хотя оба используют WebSocket. Часть правил соединения совпадает, но задачи и лимитные модели не взаимозаменяемы.
Постоянное соединение уменьшает HTTP framing и упрощает multiplexing нескольких запросов через один двунаправленный канал. Это не означает, что REST обязательно устанавливает новый TCP и TLS-сеанс для каждого вызова: нормальный HTTP-клиент переиспользует соединения через keep-alive и pooling. Интерфейс выбирают по рабочей модели, а не из предположения, что REST всегда переподключается.
Как выбирать между REST и потоками
Практическое правило остаётся полезным: если приложение собирается регулярно опрашивать меняющееся рыночное значение, подпишитесь на поток. REST или WebSocket API нужны для снимка, исторического диапазона, запроса аккаунта или торгового действия с прямым ответом.
Документация намеренно разделяет эти задачи. Поток — не база данных: он передаёт изменения после установки соединения. Локальному стакану всё равно нужна процедура snapshot плюс buffered events, а при обнаружении sequence gap состояние нужно отбросить или пересобрать.
Что именно считает потолок в 1 024 потока
Лимит относится к именам потоков на одном соединении, а не непосредственно к числу символов в получаемых данных.
Например:
btcusdt@trade— один поток для одного символа;btcusdt@tradeвместе сbtcusdt@depth— два потока;!miniTicker@arr— один поток, хотя событие может содержать обновления по множеству символов;- all-market rolling-window stream также может представлять много символов одним именем.
Поэтому широкий набор торговых пар не всегда требует по потоку на каждую пару. Сначала выберите минимальный набор stream types, который даёт необходимые данные, затем считайте реальные названия подписок. Разделяйте соединения при приближении к 1 024 или раньше, если этого требуют объём обработки, изоляция сбоев и время восстановления.
Время жизни, heartbeat и shutdown
Для актуальных Spot Streams и Spot WebSocket API:
- соединение живёт не более 24 часов;
- сервер отправляет Ping каждые 20 секунд;
- клиент должен вернуть Pong с тем же payload в течение минуты;
- unsolicited Pong не заменяет обязательный ответ;
serverShutdownприходит, когда сервер готовится завершить работу.
Истечение 24 часов и serverShutdown — разные случаи. Нельзя рассчитывать, что перед каждой штатной суточной ротацией обязательно придёт это событие. Клиент должен отдельно обрабатывать плановый reconnect, объявленный shutdown, обычный close frame и внезапный сетевой разрыв.
Для Market Streams действует лимит пяти сообщений от клиента в секунду, включая Ping, Pong и управляющие JSON-сообщения. Повторное превышение приводит к разрывам и может закончиться IP-баном. Поэтому изменения подписок нужно объединять в пакеты и ограничивать по частоте.
Python-клиент с безопасным переподключением
Следующий пример отделяет чтение сокета от обработки событий с помощью ограниченной очереди. Backoff сбрасывается только после стабильной работы соединения, используется full jitter, а ротация происходит до суточного предела.
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()
# Ping отправляет сервер. Библиотека автоматически отвечает 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())
Это каркас эксплуатации, а не готовый движок торговых данных. Переполнение очереди считается нарушением целостности, а не поводом молча выбросить события. Для depth stream одного reconnect недостаточно: локальный стакан нужно отбросить и повторить snapshot-and-buffer синхронизацию. Для ticker state можно заменять старое значение новым, но такая политика должна быть задана явно.
Частые reconnect сами по себе не называют причину. Логируйте close code и reason, возраст соединения, возраст последнего события, глубину очереди, processing lag, число подписок и исходящий маршрут. Короткие соединения вызывают heartbeat, message-rate violation, shutdown, сетевые и proxy timeouts, зависание event loop, backpressure или ошибка приложения.
Как выполнить плановую ротацию без разрыва данных
Пример выше закрывает старое соединение, а затем открывает новое, поэтому оставляет небольшой разрыв. Система с непрерывным потоком должна ротировать соединение заранее:
1. Открыть замену до достижения старым соединением 24 часов.
2. Восстановить на ней те же подписки.
3. Дождаться валидных событий от нового соединения.
4. Удалить дубли периода пересечения по trade ID, event ID или sequence.
5. Атомарно переключить downstream consumers на новое соединение.
6. Закрыть старое после сверки пересечения.
Для локального стакана новое соединение должно построить и проверить собственное состояние snapshot плюс buffer до переключения. Нельзя слепо смешивать два depth streams: пересекающиеся update IDs или незамеченный gap повредят книгу.
Production-метрики
Минимальный набор:
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}
Отдельно уведомляйте об устаревших данных, серии коротких соединений, заполнении очереди и sequence gaps. Открытый socket сам по себе не доказывает здоровье consumer.
Потоки на фьючерсной платформе
Фьючерсы используют отдельные хосты и продуктовые правила, поэтому Spot-константы нельзя без проверки переносить в общую конфигурацию.
Для USDⓈ-M у request-response сервиса есть важное отличие:
- WebSocket API
REQUEST_WEIGHTсчитается по IP; - этот WebSocket weight pool общий для USDⓈ-M WebSocket API hosts, но не объединён с REST IP weight;
ORDERSсчитается по UID и разделяется с REST;- открытие соединения WebSocket API стоит 5 weight.
Это не та же модель, что у Spot, где request weight WebSocket API входит в общий пул с REST. Для USDⓈ-M пересекаются прежде всего ордерный учёт и отдельно указанные торговые операции, а не весь IP weight.
Heartbeat и stream limits также зависят от продукта. Актуальная Spot-документация указывает Ping каждые 20 секунд и минутное окно Pong. Опубликованный документ Binance.US датирован сентябрём 2023 года и всё ещё указывает Ping раз в три минуты с десятиминутным окном. Это последняя опубликованная американская справка, но её возраст нужно фиксировать, а не подавать как поведение, заново подтверждённое в 2026 году.
Храните параметры в конфигурации с привязкой к продукту, хосту и версии документации:
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
Стабильный egress упрощает IP whitelisting, логи и атрибуцию инцидента, но отдельный адрес на каждый consumer — не первое средство против reconnect storm. Сначала нужны backoff, full jitter, плановая разнесённая ротация, ограничение concurrency и общий координатор попыток подключения. Дополнительные адреса оправданы, когда workloads действительно независимы или требуют разных сетевых идентичностей.
В этом состоит уместная роль наших прокси для Binance: выделенные IPv4, зарезервированные только для вас на срок плана и дающие предсказуемый egress для IP whitelisting и независимых workloads. Они не заменяют правильное переподключение и соблюдение лимитов Binance.