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 stock ticker?
A stock ticker aggregates live market-data updates and delivers the latest price for each symbol to many connected viewers. The difficult part is not reading one value; it is normalizing feeds, preserving per-symbol ordering, and broadcasting hot-symbol updates to only the clients that subscribed to them.
This design is for a consumer-facing viewer. It treats the latest price as a snapshot, uses a replayable stream for recovery, and makes delivery freshness and duplication explicit rather than implying trading-grade execution guarantees.
TL;DR
A stock price viewer normalizes exchange feed events, stores the latest value per symbol, and fans out updates only to interested clients. Use a durable event stream between ingestion and delivery, a latest-price cache for snapshots, and a symbol-sharded Fan-Out tier between the stream and WebSocket servers. The system is a write-broadcast workload: it favors eventual consistency and replayable delivery over trading-grade latency or execution semantics.
The difficult part is targeted fan-out. A single popular-symbol update may need to reach many connections, while unrelated WebSocket servers should not process it. REST remains useful for snapshots; one multiplexed WebSocket per client handles subscriptions and reconnects.
Scope and assumptions
This article designs a consumer-facing live price viewer: exchange adapters ingest normalized trade updates, clients read snapshots, and subscribed clients receive price changes. Order placement, market-data licensing, historical charting, regulatory audit, and execution correctness are separate systems.
The interview scenario uses illustrative assumptions:
- About 50,000 symbols and a planning envelope of up to 1 million normalized price events per second during market hours. Ten updates per second for every symbol would be about 500,000 events per second; the 1-million figure is explicit headroom, not a derived fact.
- Up to 5 million concurrent WebSocket connections, with about five symbol subscriptions per connection on average, and a highly skewed distribution in which a few symbols are much hotter than the average.
- A one-second end-to-end display target and a 200 ms snapshot target for a new subscription. These are consumer-viewer targets, not exchange or trading guarantees.
- At-least-once event delivery with sequence or timestamp checks so an older update cannot overwrite a newer cached value. A reconnect obtains a fresh snapshot and can optionally request a bounded delta.
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: If the planning envelope is 1M price events per second and each event targets 100 subscribers on average, the delivery tier performs about 100M targeted pushes per secondβa 100:1 fan-out multiplier. The 5M-connection, five-subscription figure implies 25M active subscriptions, so the actual per-symbol distribution must be measured; the 100-subscriber figure is an illustrative fan-out workload, not a universal average. This is a write-broadcast system, and the primary challenge is routing updates only to clients that care.
This distinction should be stated early: treating the system as only a read-heavy caching problem misses the delivery fan-out path.
30-second answer
Normalize every exchange message into a PriceUpdate with symbol, price, exchange timestamp, source sequence, and ingest timestamp. Publish it to Kafka, materialize latest:{symbol} in Redis, and route the event through a symbol-sharded Fan-Out service. WebSocket servers register their interested symbols and push only targeted updates; REST and new subscriptions read the latest snapshot. Reconnects resynchronize from Redis, and consumer-facing delivery remains eventually consistent.
5-minute explanation
The write path starts with exchange adapters that validate and normalize incompatible feed formats. The normalized event is acknowledged after durable publication, then separate consumers update the latest-price cache, persist any required history, and deliver updates. Keeping those consumers separate avoids coupling exchange-feed throughput to the number of connected clients.
The read path has two shapes. GET /prices/{symbol} reads one current snapshot. A client opens one WebSocket, subscribes to multiple symbols, receives snapshots immediately, and then receives deltas. WebSocket servers keep only ephemeral local subscription state; a Fan-Out tier owns symbol routing and forwards a price event only to servers with at least one interested client.
Kafka provides a replay window for consumer recovery, while Redis provides the current value for a client joining mid-stream. Delivery is at least once and can be duplicated; clients or servers should use a per-symbol sequence or exchange timestamp to discard older updates. A reconnect never relies on missed push messages alone: it reads the current snapshot and re-registers subscriptions.
45-minute interview approach
Use this section as the pacing plan for a stock-ticker interview; it is not a reading-time promise.
- 0-5 minutes β clarify the contract: Confirm whether the viewer needs trades or quotes, supported instruments and currencies, freshness, reconnect behavior, maximum subscriptions, and whether order execution is out of scope.
- 5-10 minutes β requirements and estimates: State the illustrative 1M normalized events/second, 5M concurrent connections, five subscriptions per connection, one-second delivery target, and 200 ms snapshot target.
- 10-15 minutes β entities and APIs: Define
PriceUpdate,LatestPrice,Symbol, andSubscription; sketch REST snapshot, WebSocket subscribe, unsubscribe, and reconnect messages. - 15-25 minutes β baseline architecture and flows: Draw exchange adapters, Kafka, latest-price materialization, Fan-Out, WebSocket servers, Redis, and the REST API. Walk through one tick and one new subscription.
- 25-35 minutes β choose deep dives: Compare direct broadcast, Redis Pub/Sub, and a dedicated symbol-sharded Fan-Out tier; then compare long polling, SSE, and multiplexed WebSockets.
- 35-41 minutes β reliability, security, and operations: Cover sequence ordering, replay, backpressure, slow consumers, connection health, feed gaps, symbol validation, authorization, and metrics for lag and dropped updates.
- 41-45 minutes β trade-offs and close: Explain freshness versus bandwidth, hot-symbol isolation, cache behavior on reconnect, retention, and what would change for historical data or trading.
Core Entities
- PriceUpdate: A single normalized trade event; contains symbol, price, volume, exchange timestamp, source sequence (when supplied), and ingest timestamp.
- LatestPrice: The newest accepted price per symbol; cached in Redis with its source sequence or timestamp and ingest timestamp; 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.
The entity list stays short because the main complexity is the delivery pipeline rather than a large business data model. The schema deep dive should support that pipeline without displacing it.
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" }
The reconnect action is useful because mobile clients can drop connections. Without a resynchronization step, a client that loses signal can display its last value until another update arrives for each symbol.
High-Level Design and critical flows
1. View current price
The simplest system that satisfies FR1: read the latest cached price for a symbol and return it.
Components:
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.