Stock Ticker
Design a system that aggregates live stock prices from multiple exchanges worldwide and serves them to millions of users with sub-second latency, covering feed normalization, pub-sub fanout, WebSocket delivery, and cache strategies.
What is a real-time stock price viewer?
A stock price viewer aggregates live trade data from exchanges like NYSE, NASDAQ, and CBOE and displays current prices to millions of users watching their portfolios. The apparent simplicity hides a brutal engineering challenge: during market hours, 50,000 symbols each tick up to 10 times per second, producing roughly 1 million price events per second. Each of those events must fan out to potentially millions of subscribed clients within one second.
This is a write-broadcast problem, and it tests every layer from feed normalization through pub-sub routing to WebSocket delivery.
Functional Requirements
Core Requirements
- Users can view the current price for any listed stock or ETF.
- Prices update in near real time, within one second of a trade.
- Users can watch a portfolio of symbols simultaneously on a single connection.
Below the Line (out of scope)
- Historical charting with full OHLCV data
- Order execution or trading
Historical charting is below the line because it does not touch the live feed path at all. To add it, we would stream all normalized price events to a columnar time-series store (ClickHouse or TimescaleDB) and serve historical queries from a separate read API, a background pipeline sitting beside the live viewer rather than inside it.
Order execution is out of scope because placing orders requires an order routing layer, a matching engine, real-time risk checks, and regulatory compliance infrastructure. That is an entirely separate system that consumes price data from this one rather than sharing its internals.
The hardest part in scope: Fan-out at scale. One price update for AAPL during a volatile market must reach potentially millions of subscribed clients within one second. Every decision about the pub-sub layer, WebSocket servers, and delivery protocol traces back to this fan-out challenge.
Non-Functional Requirements
Core Requirements
- Latency: Price updates delivered to clients within 1 second of a trade. New subscribers receive an immediate snapshot of the current price within 200ms of opening a connection.
- Throughput: Ingest up to 1M price events per second across all symbols during market hours.
- Scale: Support 10M DAU with up to 5M concurrent WebSocket connections at peak. Each symbol averages 100 subscribers but popular symbols like AAPL can reach millions.
- Availability: 99.9% uptime during market hours. A client seeing a price that is one tick stale for a moment is acceptable. A client disconnected entirely for minutes is not.
- Consistency: Eventually consistent. Correctness within the 1-second window is sufficient for a consumer viewer.
Below the Line
- Sub-millisecond latency (that is co-location trading infrastructure, not a consumer application)
- Financial compliance audit logs
- Cross-currency normalization across global exchanges
Read/write ratio analysis: The system generates 1M price events per second during market hours. With 5M concurrent subscribers each watching an average of 5 symbols, and approximately 100 subscribers per symbol, every price update on average fans out to 100 delivery operations. That makes the effective delivery rate 100M targeted pushes per second against 1M source writes, a 100:1 fan-out multiplier. This is not a read-heavy system in the traditional sense; it is a write-broadcast system. The primary design challenge is not serving reads cheaply, it is routing writes efficiently to only the clients who care.
I call out this distinction early in every stock-price interview because candidates who treat it as a read-heavy caching problem design the wrong system entirely.
Core Entities
- PriceUpdate: A single trade event from an exchange; contains symbol, price, volume, and exchange timestamp.
- LatestPrice: The most recent price per symbol; cached in Redis and updated on every tick; the primary read target for snapshots.
- Symbol: A tradeable instrument (stock or ETF); identifies which exchange lists it and the minimum tick size.
- Subscription: An in-memory mapping from a connected client to the set of symbols it is watching; ephemeral and lives only on the WebSocket server.
Schema and indexing decisions are deferred to a data-model deep dive. These four entities are enough to anchor the design through High-Level Design.
I keep the entity list short here because the real complexity in this system is not the data model. It is the delivery pipeline. In an interview, spending five minutes on price schemas when the interviewer wants to hear about fan-out is a common trap.
API Design
Start with one endpoint per functional requirement, then evolve where the naive shape breaks.
FR 1 -- View current price (REST snapshot):
GET /prices/{symbol}
Response: { symbol, price, currency, exchange_timestamp, server_timestamp }
A simple REST GET is the right shape for an on-demand snapshot. The response includes both exchange timestamp (when the trade occurred) and server timestamp (when the system ingested it) so clients can compute propagation lag for display.
FR 2 -- Real-time updates:
Naive approach: poll the REST endpoint every second per symbol.
GET /prices/{symbol}
// Client calls this every 1,000ms for each watched symbol
This breaks immediately. With 10M users each watching 5 symbols and polling every second, the system absorbs 50M HTTP requests per second purely to deliver updates that may not have changed. The evolved approach is a persistent server-push connection.
Evolved approach: WebSocket connection with subscription commands.
WS /stream
// Client subscribes after opening connection
{ "action": "subscribe", "symbols": ["AAPL", "MSFT", "TSLA"] }
// Server immediately sends a snapshot per subscribed symbol
{ "type": "snapshot", "symbol": "AAPL", "price": 185.42, "timestamp": "2026-03-29T14:30:01.234Z" }
// Server then streams deltas as prices change
{ "type": "update", "symbol": "AAPL", "price": 185.43, "timestamp": "2026-03-29T14:30:01.891Z" }
A single WebSocket connection per client multiplexes all symbol subscriptions over one TCP connection. Subscription changes (add or remove a symbol) travel over the same connection without a separate HTTP round trip.
FR 3 -- Watch multiple symbols and reconnect handling:
WS /stream
// Subscribe to multiple symbols in one message
{ "action": "subscribe", "symbols": ["AAPL", "MSFT", "GOOGL", "NVDA"] }
// On reconnect after a drop, client sends last-seen timestamps per symbol
{ "action": "reconnect", "last_seen": { "AAPL": "2026-03-29T14:30:05.000Z", "MSFT": "2026-03-29T14:30:05.100Z" } }
// Server sends snapshots only for symbols whose price changed during the gap
{ "type": "snapshot", "symbol": "AAPL", "price": 186.10, "timestamp": "2026-03-29T14:30:07.220Z" }
I always include the reconnect action in the WebSocket protocol because clients on mobile networks drop connections frequently. Without delta-on-reconnect, a user who loses signal for 30 seconds sees prices frozen at their last value until the next natural tick on each symbol.
High-Level Design
1. View current price
The simplest system that satisfies FR1: read the latest cached price for a symbol and return it.
Components:
- Client: Web or mobile application sending
GET /prices/{symbol}requests. - Price API Server: Validates the symbol and routes the read to the cache first.
- Redis Price Cache: Holds the latest price per symbol as a key-value entry (
latest:AAPL). Sub-millisecond reads. The Exchange Ingestion process updates this key on every tick. - Price Store: Persistent fallback for the case where Redis restarts or a symbol has never been written (cold start).
- Exchange Ingestion: Background service normalizing feeds from multiple exchanges and writing the latest price into Cache and Store on every tick. Treated as a black box here; the normalization design is addressed in the High-Level Design for FR2.
Request walkthrough:
- Client sends
GET /prices/AAPL. - Price API Server validates the symbol is in the known symbol list.
- Server reads
latest:AAPLfrom Redis Price Cache (under 1ms). - On cache miss, server reads from Price Store as a fallback.
- Server returns
{ symbol, price, exchange_timestamp, server_timestamp }.
The cache hit rate approaches 100% after market open because Exchange Ingestion writes every symbol on the first tick; the Price Store is a safety net for cold starts, not a primary path.
I spend about 30 seconds on this diagram in an interview, just enough to show I can handle the trivial case, then move immediately to the real-time delivery challenge.
2. Real-time price updates
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.