Market data at scale
Why market data is the exchange's hardest scaling problem (not matching), and the two tools that tame it: tiered feeds and conflation.
The authoritative log emits ordered facts. Market data is the largest audience for those facts, but not every audience buys the same product or reads at the same speed.
The obvious shortcut is to have the matching engine publish directly to every subscriber and retain every update in an unbounded per-client queue. Yet consumer pressure never blocks matching; public state may be conflated for a slow audience, while private order and fill facts remain complete and replayable.
Run the numbers. The core produces perhaps 500k book updates/second at peak. Matching handles that in one process. But market data must deliver those updates to every subscriber: 500k updates × 200k concurrent subscribers is 1011 messages per second of naive fan-out. The bottleneck of an exchange at scale is not deciding trades — it's telling everyone about them.
Tool 1 — tiered feeds. Different consumers need radically different fidelity, so we sell different products:
| Tier | Content | Volume | Consumer |
|---|---|---|---|
| L1 | Best bid/ask + last trade | Tiny | Retail apps, tickers — the vast majority of users |
| L2 | Aggregated depth, top 10–20 price levels | Moderate | Active traders, charting tools |
| L3 | Every individual order event, add/modify/cancel | Full firehose | Market makers, HFT — a few hundred firms |
| Delayed | Any of the above, 15 minutes late | — | Free tiers, websites |
Tool 2 — conflation. A phone on hotel Wi-Fi cannot drink from even the L1 firehose during a volatile minute. Rather than queue unboundedly (memory death) or disconnect the client (availability death), the feed server conflates: it keeps only the latest state per symbol, and when the slow client is ready, sends current truth — skipping the intermediate churn the client no longer cares about.
flowchart TB
ME["Matching engine"] --> MDP["Publisher: builds L1 / L2 / L3 streams"]
MDP --> F1["Feed servers — L3 firehose"]
MDP --> F2["Feed servers — L1/L2 fan-out tier"]
MDP --> DLY["Delay buffer — 15 min"]
F1 --> MM["Hundreds of firms — every message, gap-detected"]
F2 --> FAST["Fast consumers — every update"]
F2 --> SLOW["Slow consumers — conflated: latest state per symbol"]
DLY --> FREE["Millions of free-tier users via CDN-style edge caches"]
Conflation works because L1/L2 data is state, not history — a client showing prices needs the current best bid, not every bid that briefly existed. L3 consumers, who genuinely need history, get sequence-numbered streams with gap detection instead (the mechanics arrive in lesson 2.3). Note the asymmetry with private data: your own fills are never conflated. Public state can be summarized; personal facts cannot.
A retail phone stops reading for 30 seconds. Preserving every intermediate quote can exhaust memory, while dropping an account fill would erase a fact. The correct degradation sends the newest public state and retains the complete private sequence.
Fan-out, not matching, is the scale bottleneck: messages × subscribers. Tier the product: L1/L2/L3/delayed serve different needs at different volumes. Conflate public state for slow consumers; never conflate private facts.