# How to stream live trades from several exchanges

One process, several venues' trade websockets, one record shape and reconnects that do not lose prints silently. Which library or feed to use, and what breaks.

*https://cryptomarkets.tools/how-to/stream-trades-from-several-exchanges · next to Crypto Market Data APIs*

**Answer:** Use CCXT's websocket classes: one MIT library, one watchTrades loop per venue, no key for public trades, supported by 73 of its 76 websocket exchange classes. Tardis.dev's free Node client adds your arrival time beside the exchange's; CoinAPI sells one paid connection over many venues. The part that breaks is continuity: libraries reconnect silently, Binance drops every connection at 24 hours, and only some venues number trades so a gap can be seen.

## Approaches

*In the author’s order. Paid placement does not affect it.*

1. [CCXT](https://cryptomarkets.tools/tools/ccxt.md) — One MIT library and one loop per venue, watchTrades on 73 of 76 websocket exchange classes, no key for public streams, reconnection done for you.
2. [Tardis.dev](https://cryptomarkets.tools/tools/tardis-dev.md) — A free, keyless Node client and local server that normalise many venues into one trade record carrying both the exchange clock and your arrival clock.
3. [CoinAPI Market Data API](https://cryptomarkets.tools/tools/coinapi.md) — One paid connection instead of one per venue, a single symbol scheme over 400+ venues, billed per GiB streamed, with its own UUIDs in place of venue trade ids.
4. [ccapi](https://cryptomarkets.tools/tools/ccapi.md) — Header-only C++17 over 31 venue identifiers, exchange time and receive time on every message, and a connection closed rather than a bad book passed on.
5. [Binance Market Data API](https://cryptomarkets.tools/tools/binance-api.md) — The venue's own trade and aggTrade streams, free and unkeyed, with trade ids you can check for gaps and a disconnect every 24 hours by design.
6. [Bybit Market Data API](https://cryptomarkets.tools/tools/bybit-api.md) — A second dialect for the same job — you send the pings, books arrive as a snapshot then deltas, and the trade id cannot show you a gap.

## The short way

[CCXT](https://cryptomarkets.tools/tools/ccxt), the websocket half of it that used to be sold as CCXT Pro. The manual now
opens with "CCXT Pro is a free part of CCXT", and the 4.5.90 wheel of 23 October 2026 declares MIT
and ships 76 websocket exchange classes under `ccxt.pro`, 73 of which report `watchTrades`. One
coroutine per venue, one `asyncio.gather` over them, no key:

```python
import asyncio
import ccxt.pro as ccxtpro

VENUES = {'binance': 'BTC/USDT', 'okx': 'BTC/USDT', 'kraken': 'BTC/USD'}

async def stream(exchange_id, symbol):
    exchange = getattr(ccxtpro, exchange_id)({'newUpdates': True})
    try:
        while True:
            try:
                trades = await exchange.watch_trades(symbol)
            except Exception as e:
                print(exchange_id, type(e).__name__, e)
                await asyncio.sleep(1)
                continue
            for t in trades:
                print(exchange_id, t['id'], t['timestamp'], exchange.milliseconds(),
                      t['side'], t['price'], t['amount'])
    finally:
        await exchange.close()

async def main():
    await asyncio.gather(*(stream(e, s) for e, s in VENUES.items()))

asyncio.run(main())
```

Every venue comes back in one record: a string `id`, an integer `timestamp` in milliseconds from
the exchange, `side`, `price`, `amount`, and the venue's original message under `info`. The
library holds the connection, answers the venue's pings and reconnects with backoff; when a
connection drops, the exception surfaces in your loop and the next `watch_trades` call is what
reconnects it, which is why the `try` sits inside the `while`.

Two lines in that file are the ones a first version leaves out. `newUpdates: True` makes each
call return only the trades that arrived since the last one; without it the call returns the
library's cache — up to 1,000 trades by default — and a loop that writes every row it is handed
writes the same trades again on every pass. And `exchange.milliseconds()` beside each row is your
own arrival time, which the unified record does not carry and which you cannot reconstruct later.

For many symbols on one venue, `watch_trades_for_symbols` takes a list on one subscription, on the
34 classes that report it — Binance's caps it at 200 symbols a call.

## What the options are

**One library, venues as a parameter.** CCXT, as above. Read what the unified call chose for you
on each venue. On Binance it subscribes to the raw `trade` stream unless you pass `name: 'aggTrade'`;
it spreads subscriptions round-robin over up to 50 connections and allows 200 per connection; and
its `watchOrderBook` fetches a 1,000-level REST snapshot for every symbol it starts. None of that is
wrong, and all of it is a default that decides how much of a venue's budget a restart spends.

**A normaliser that keeps both clocks.** [Tardis.dev](https://cryptomarkets.tools/tools/tardis-dev) sells history, not a
feed, and its open-source Node client streams live data for nothing: `streamNormalized` connects
straight to each venue's websocket, restarts connections that close or go quiet for ten seconds,
and `combine` merges several venues into one iterator. Its trade record is `id` "if provided by
exchange", `price`, `amount`, `side` as buy, sell or unknown, `timestamp` from the exchange and
`localTimestamp` from your machine — the arrival clock CCXT leaves to you. Pass
`withDisconnectMessages` and every drop arrives as a message of its own, which is the gap log you
would otherwise have to build. If the rest of the stack is not JavaScript, the `tardis-machine`
server runs locally and serves the same thing as one websocket, `/ws-stream-normalized`, for every
venue you list; the docs say it does not require a Data API key, and that its real-time output is
normalised only. The Python client does not stream at all. The real-time feed table in the Node
source lists 65 exchange ids, and FTX, CoinFLEX, Serum and Mango are still among them, so check
the venue is alive before you read anything into a quiet socket.

**One connection to somebody else.** [CoinAPI](https://cryptomarkets.tools/tools/coinapi) is the paid version of the same
idea, run on its servers: you open one websocket, send a hello message naming the data types and
filters, and every venue arrives in one schema with `BINANCE_SPOT_BTC_USDT`-style symbols. Its
trade carries `time_exchange` and `time_coinapi` to seven decimal places, a `uuid` that is CoinAPI's
own identifier rather than the venue's trade id, and a `taker_side` that can read `BUY_ESTIMATED`
or `SELL_ESTIMATED` — when the venue did not report the aggressor, CoinAPI infers one. The meter is
bytes: Startup at 79 dollars a month includes 32 GiB a day of trades, quotes and books, and
Pay As You Go bills them at a dollar a GiB. The card records that a subscription typically allows
one concurrent session, so development and production want two accounts rather than two keys.

**The same idea in C++.** [ccapi](https://cryptomarkets.tools/tools/ccapi) subscribes with `Subscription("bybit", "BTCUSDT",
"TRADE")` over 31 venue identifiers, with `AGG_TRADE` for the Binance family, and every message
carries `time`, the exchange's timestamp, beside `timeReceived`, the library's. What it does with a
book that fails its own checks is the useful detail: the source closes the connection on a checksum
mismatch or a crossed book and reports `INCORRECT_STATE_FOUND`, instead of passing a corrupt book
downstream. The cost is the build — no published package in any language, and three of the 31
identifiers belong to venues that stopped trading this year.

**A Go engine with the connections inside it.** [GoCryptoTrader](https://cryptomarkets.tools/tools/gocryptotrader) keeps its
own exchange connectors, and its `websocket` package does the plumbing this page is about —
connection monitoring with reconnect, a traffic monitor that reconnects when nothing arrives for a
configured period, subscription management and rate limiting. Its README table shows websockets on
20 of 23 exchanges, and its trade record is a string `TID`, side, price, amount and a Go
`time.Time`. It is an engine you run rather than a library you import, it has never cut a release
or a tag, and its README says it is not ready for production — the maintainers' words, not ours.

**The venue directly.** [Binance](https://cryptomarkets.tools/tools/binance-api) and [Bybit](https://cryptomarkets.tools/tools/bybit-api) both stream
trades to an unauthenticated client, and the reason to go direct is the fields and the controls
a wrapper flattens. Binance serves market data alone on `wss://data-stream.binance.vision`, beside
the main `wss://stream.binance.com:9443`; you subscribe on an open connection with:

```json
{
  "method": "SUBSCRIBE",
  "params": ["btcusdt@trade", "btcusdt@aggTrade"],
  "id": 1
}
```

Symbols in stream names are lowercase, and each `trade` event carries an integer trade id `t`,
price, quantity, trade time `T` and `m`, whether the buyer was the maker. Bybit splits the same job
by product line — `wss://stream.bybit.com/v5/public/spot`, `/linear`, `/inverse`, `/option` — and
subscribes with `{"op": "subscribe", "args": ["publicTrade.BTCUSDT"]}`. The dialects disagree on
the small things that kill a connection: Binance pings you every 20 seconds and drops you if no pong
comes back within a minute, while Bybit expects *you* to send `{"op": "ping"}`, every 20 seconds by
its recommendation. [OKX](https://cryptomarkets.tools/tools/okx-api) is a third dialect again: the string `ping` before 30
seconds of silence. Writing this yourself means writing it three times, which is the whole case for
the libraries above.

## Where this breaks

**A trade stream is not always a stream of trades.** Binance publishes two, and they differ by
definition rather than by speed: `@trade` pushes "raw trade information; each trade has a unique
buyer and seller", while `@aggTrade` aggregates the fills of a single taker order and carries the
first and last trade ids it covers. OKX goes the other way round and names its aggregated channel
`trades` — one message per taker order, filled price and source, with `count` matches folded in and
`tradeId` set to the last of them — and puts one-trade-per-message on `trades-all`, on the business
URL. Run the snippet above for twenty seconds and OKX's ids skip while Binance's arrive in sequence;
neither feed dropped anything. Counting trades, sizing them or building a trade-count bar across two
venues without reading both pages compares two different things under one column name.

**The exchange's timestamp and yours are two measurements.** Unified records keep the venue's clock
at millisecond resolution; Binance will send microseconds only if you add `timeUnit=MICROSECOND` to
the URL. The arrival time is the one number only your recorder can produce, so take it at the socket,
before parsing, or the figure you store is the age of your parser's queue. And check the sign: on the
same twenty-second run, Kraken's trades arrived 67 milliseconds *before* their own exchange timestamp,
which does not mean Kraken is early. It means one of the two clocks is wrong, and the only one you
can fix is yours. CoinAPI's documentation gives the same instruction for the same reason — make sure
your clock is synchronised and does not drift — and it applies before you treat exchange time minus
arrival time as latency. The archive version of this,
with both clocks in a file, is in [the backfill how-to](https://cryptomarkets.tools/how-to/backfill-tick-level-trade-history).

**Reconnects are silent, and they are scheduled.** Every library on this page reconnects for you,
which is the problem: the trades printed while the socket was down never arrive, and nothing in the
next message says so. Binance drops every connection at the 24-hour mark by design and sends
`serverShutdown` when a server is about to shut down, so a recorder that runs for a week has at least
seven reconnects in it, each of them a possible gap. Whether you can see a gap depends on the venue. Binance's raw trade id is a per-symbol integer:
remember the last one, and a jump after a reconnect is a measured hole. `GET /api/v3/historicalTrades` fills it trade by trade from a `fromId` at weight
25; `aggTrades` does it at weight 4, but in aggregated form. On OKX's aggregated channel the
check has to add `count` before it calls anything a gap. Bybit's linear feed sends a UUID as the trade
id and documents `seq` as a cross sequence, so neither is a counter, and the REST fallback returns
only the most recent 60 spot trades or 1,000 for contracts — a gap longer than that waits for the
next day's file. CoinAPI's `sequence` is per data type and symbol and "valid only for the lifespan of
the connection", and a slow consumer is its own cause of loss: CoinAPI buffers 131,072 messages per
connection, then disconnects you and does not resend what was buffered. Log every disconnect with its
start and end time; on the venues you cannot refill, that log is the only honest record of the hole.

**A book diff is useless without the snapshot it applies to.** Trades are self-contained; depth
updates are not. Binance's procedure has you buffer the diff stream, fetch a REST snapshot, discard
events with `u` at or below the snapshot's `lastUpdateId`, and then require each event's `U` to be no
more than one past the last `u` you applied — otherwise "discard your local order book and restart
the process from the beginning". On a live stream, unlike in an archive, you can do that, but it costs
REST weight each time: 50 for a 1,000-level snapshot, which is what CCXT's Binance class requests per
symbol, and 250 for 5,000. Reconnect a hundred books at once and that is most of a minute's budget
from one IP. Bybit inverts the work — it pushes a snapshot when you subscribe and another whenever its
side has a problem, and a `u` of 1 means its service restarted and your book must be overwritten — and
leaves Retail Price Improvement orders out of the book entirely, so a book rebuilt from it will not
explain every fill on the trade stream.

**The limits are per IP and per connection, and a restart loop finds them.** Binance allows 300 new
connections per IP every five minutes, 1,024 streams on one connection and five incoming messages a
second on it — pings, pongs and subscribe messages all count, and a connection over the limit is
disconnected. Bybit allows 500 new connections in five minutes per websocket domain, ten args per
subscribe request on spot, and from 26 November 2026 a hundred args per connection on spot and
futures: a subscription list that fits on one socket today needs splitting by then. A container that
crash-loops through reconnects, or a fleet behind one NAT address restarting together, spends the
connection budget before it spends anything else. [Rate limit](https://cryptomarkets.tools/glossary/rate-limit) covers what each
of these counters is counting, and the four large venues' websocket limits sit side by side in
[the four-venue API comparison](https://cryptomarkets.tools/compare/binance-vs-coinbase-vs-bybit-vs-okx-apis).

**Where the process runs decides which venues it can open.** Bybit's own error table lists "You are
using U.S IP" among the causes of a 403, and its connect page sends accounts registered on regional sites —
Turkish, Indonesian, Kazakh, Georgian and Japanese among them — to hostnames of their own. CCXT
ships `binanceus`, `bybiteu`, `okxus` and `myokx` as classes separate from the global ones, with
hosts of their own. Test the stream from the region it will be deployed in, not from a laptop
somewhere else: a block by IP arrives before the first subscription does, so none of the handling
above ever has a message to read.

**And a recording is not a backfill.** What a recorder holds starts on the day you started it, has a
hole wherever it was down, and carries the clock of whichever machine it ran on. That is often
exactly right — it is the only way to own an arrival clock — but the moment the question needs
anything older than your first message, or needs to be defensible across gaps, it has become a
history purchase, and [how to backfill tick-level trade history](https://cryptomarkets.tools/how-to/backfill-tick-level-trade-history)
is the page for that. Recording also does not change who owns what you recorded: the venue's market
data terms still govern what you may show anybody else, and
[what exchange API terms actually let you do](https://cryptomarkets.tools/guides/exchange-api-terms) works through them.

## If you outgrow this

If the problem is **the number of sockets**, there are two directions and they are opposites. Split
the process by venue, one worker and one egress address per venue, so that a ban or a restart storm on
one does not take the rest down; or stop holding sockets at all and buy the single connection CoinAPI
sells, and pay for it per GiB.

If the problem is **that live and history must be the same series**, Tardis.dev is the one product
here built for it: `replayNormalized` returns the archive in the same record `streamNormalized`
produces live, so the code that consumes today's stream can be pointed at last year's. The archive is
the paid half, priced on its card.

If the problem is **latency**, the Python event loop is the first thing to go — ccapi exists for that
reader — and the second is distance, which no library fixes. What "real time" can mean on each
provider's tier is set out on [Crypto Market Data APIs](https://cryptomarkets.tools/categories/market-data-apis) and not repeated
here.

## FAQ

### Do I need an API key to stream public trades?

Not from the venues. Binance, Bybit and OKX publish their trade streams to an unauthenticated client, and CCXT, ccapi and Tardis.dev's open-source client connect to those streams directly, so none of them asks for a key on public data. Tardis.dev's documentation says its local server's real-time endpoint does not require a Data API key for the same reason. CoinAPI is the exception on this page, because it is a feed of its own — its hello message carries your key.

### Why do trade ids skip on OKX when nothing was dropped?

Because OKX's `trades` channel is aggregated. It sends one message per taker order, filled price and order source, puts the last trade id of the aggregation in `tradeId`, and says how many matches it covers in `count` — so `tradeId` 123 with `count` 3 stands for 121, 122 and 123. One trade per message is a different channel, `trades-all`, on the business URL rather than the public one.

### How do I tell that I missed trades during a reconnect?

Only on venues whose trade ids run in sequence. On Binance's raw `trade` stream the id is an integer per symbol, so a jump after a reconnect is a measurable gap that the REST `aggTrades` or `historicalTrades` endpoints can fill from a known id. Bybit's linear feed sends a UUID per trade and its `seq` is a cross sequence rather than a counter, and CoinAPI's `sequence` restarts with every connection, so on those the honest record is a logged disconnect with a start and an end.

### Is recording the stream a way to build my own trade history?

From the moment you start, and only for as long as the recorder stays up. Everything before the first message, and everything inside any reconnect you could not refill, is missing from it for good. What a recording gives you that an archive does not is your own arrival clock; what you may do with it afterwards is still decided by the venue's licence, not by the fact that you recorded it.

## Sources

1. [WebSocket Streams for Binance — general information, limits, trade and aggregate trade streams, local order book](https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md) — Binance, read 2026-10-27
2. [Connect, V5 WebSocket — endpoints, IP limits, args limits, heartbeat](https://bybit-exchange.github.io/docs/v5/ws/connect) — Bybit, read 2026-10-27
3. [Trade, V5 public WebSocket stream](https://bybit-exchange.github.io/docs/v5/websocket/public/trade) — Bybit, read 2026-10-27
4. [API v5 — WS / Trades channel and WS / All trades channel](https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-trades-channel) — OKX, read 2026-10-27
5. [CCXT Pro Manual — streaming specifics, caching layer, watchTrades](https://raw.githubusercontent.com/ccxt/ccxt/master/wiki/ccxt.pro.manual.md) — CCXT, read 2026-10-27
6. [Streaming Real-Time Data, tardis-machine](https://docs.tardis.dev/tardis-machine/streaming-real-time-data) — Tardis.dev, read 2026-10-27
7. [WebSocket V1 API — Messages](https://www.coinapi.io/products/market-data-api/docs/websocket/messages) — CoinAPI, read 2026-10-27
8. [WebSocket V1 API — General, hello message and data buffering](https://www.coinapi.io/products/market-data-api/docs/websocket/general) — CoinAPI, read 2026-10-27

*Last updated 2026-10-27. Corrected in place — an endpoint that moves is a bug here, not a new post.*
