System Design: Live Stream Reactions
During live events (Apple keynotes, Twitch streams), users can send reactions (hearts, claps) that float across everyone's screen. The challenge is ingesting tens of thousands of clicks per second and broadcasting them with sub-second latency.
Ingestion and Local Buffering
If millions of users click "heart", we cannot send every click to the database. The client app throttles clicks, sending a batch (e.g., "5 hearts") every second. The API gateway receives these and routes them to an ingestion service. The ingestion service buffers these counts in memory for 1-2 seconds, aggregating the total counts per reaction type.
Pub/Sub Broadcast
The aggregated counts (e.g., "Stream123: +500 hearts, +200 claps") are published to a Redis Pub/Sub topic. The WebSocket servers handling the viewer connections subscribe to this topic. When they receive the aggregated update, they push it down to the clients. The client-side JavaScript then renders the floating animations based on these aggregated numbers, creating the illusion of continuous individual clicks.
Decoupled Analytics
Do not make volatile buffers the only accepted record if permanent counts matter. Persist an event or idempotent delta before acknowledging it, then derive coarse broadcast updates separately. If Redis Pub/Sub drops an update or a WebSocket server restarts, clients should refetch a versioned snapshot and resume from a sequence boundary. Batching lowers fanout bandwidth, but it sacrifices one-to-one animation fidelity and adds up to one batch interval of delay; bound per-client queues and drop visual updates rather than exhausting memory for a slow viewer.
Worked example: why aggregation is mandatory
A live event has 2 million concurrent viewers; 5 percent react each second.
| Design | Messages delivered per second |
|---|---|
| Forward every reaction to every viewer | 100,000 reactions x 2,000,000 viewers = 200 billion, impossible |
| Aggregate per stream every 500 ms, send one summary to each viewer | 2 updates x 2,000,000 viewers = 4 million |
| Spread over 200 edge servers at 10,000 connections each | 20,000 messages per second per server, very manageable |
On the ingest side, clients batch taps locally (for example, send "5 hearts" once per second), so 100,000 reactions per second become at most one request per active user per second, and ingest nodes sum them per stream before publishing.
Fan-out design
| Approach | How it routes | Trade-off |
|---|---|---|
| Broadcast to all edge servers | Every server receives every stream's updates | Simple; wasteful with thousands of concurrent streams |
| Subscription registry | Edge servers register "I have viewers of stream S"; dispatcher sends only there | Efficient; registry must handle churn as viewers join and leave |
| Pub/sub channel per stream, such as Redis Pub/Sub or a Kafka partition | Edge servers subscribe to channels for their streams | Leverages existing infrastructure; hot channels need sharding |
LinkedIn's talk describes the subscription approach: frontend nodes hold persistent connections and track which live videos their clients watch, and a dispatcher layer forwards each like only to frontend nodes with subscribers for that video, including across data centers.
Durability vs display
If the reaction total is a permanent metric, write an idempotent delta (stream ID, ingest node, batch sequence) to a durable log before acknowledging, and derive the displayed counter from that log. The animation path can be lossy: if a pub/sub message is dropped or an edge server restarts, clients simply fetch the current total and continue. Protocol choice: server-sent events suit one-way push of summaries; WebSockets suit apps that also send reactions and comments over the same connection.