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.

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.

The short way

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:

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 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 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 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 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 and Bybit 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:

{
  "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 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.

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 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.

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 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 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 and not repeated here.

The tools named above

In the order this page puts them in, which is an editorial judgement and not a ranking anyone paid for.

  1. CCXT

    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.

    One MIT client for 104 crypto exchanges, 76 of them over websocket.

    FreeFree tierOpen source

  2. Tardis.dev

    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.

    Tick-by-tick order books, trades and options chains from 69 exchange feeds since 2019.

    $350/moFree tier

  3. CoinAPI Market Data API

    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.

    One symbol scheme over 400+ venues, metered in REST credits and WebSocket gigabytes.

    $79/mo

  4. ccapi

    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.

    Header-only C++ market data and execution for 31 venue ids, on a Bloomberg-shaped API.

    FreeFree tierOpen source

  5. Binance Market Data API

    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.

    One venue's own tape — prices, books, trades and funding, free and unkeyed.

    FreeFree tier

  6. Bybit Market Data API

    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.

    Venue-direct spot, perps and options data with no key — if your IP is not in the US.

    FreeFree tier

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 — Binance, read
  2. Connect, V5 WebSocket — endpoints, IP limits, args limits, heartbeat — Bybit, read
  3. Trade, V5 public WebSocket stream — Bybit, read
  4. API v5 — WS / Trades channel and WS / All trades channel — OKX, read
  5. CCXT Pro Manual — streaming specifics, caching layer, watchTrades — CCXT, read
  6. Streaming Real-Time Data, tardis-machine — Tardis.dev, read
  7. WebSocket V1 API — Messages — CoinAPI, read
  8. WebSocket V1 API — General, hello message and data buffering — CoinAPI, read

The catalogue next door

This page names a handful of products. The rest of them are in Crypto Market Data APIs, each filled in against the same schema, with the fields to narrow it yourself.

Last updated . Corrected in place — an endpoint that moves is a bug on this page, not a new post.

Report an error on this page

Quote the line and say what it should be.

The vendor’s page, filing or documentation that says otherwise.

Only if you want to hear back. Never published.

Read by a person. It fixes a fact and moves nothing else.

Anything else — a question this page leaves open, a tool it should be compared with? Write to feedback@cryptomarkets.tools.