Library
34 min read

Design live cricket score fan out

Last over of an IPL final. Tens of millions of people are watching a number that changes about once every thirty seconds, and every one of them wants it within a second of the ball being bowled.

The interesting thing about this question is how lopsided it is. One person types the score in. Tens of millions of phones read it. Watch one ball travel from the scorer to the phones.

ScorerIngestMatch storeUpdate busSnapshot builderCDNEdge serversViewersCatch up APIConnection LB

Ball. One scorer taps in the ball. That one keystroke is the entire write path of this system.

A few words come up all through this page:

  • Fan out means taking one message and copying it to many receivers. Here, one ball becomes tens of millions of screen updates.
  • Poll means the phone keeps asking "anything new?" on a timer. Push means the phone keeps a connection open and the server sends news down it the moment it happens. Push vs pull covers the general trade.
  • An edge server is one of the machines that holds the open connections to phones. The phones only ever talk to these.
  • A version is a number that goes up by one with every update. It is how a phone knows which of two messages is newer.

The write rate is almost nothing: a few events per over from one scorer. The read rate is one of the largest concurrent workloads anyone runs. That ratio, roughly one write to a hundred million reads, is unlike anything else in this course, and it flips the usual instincts: caching is easy here, and the hard part is holding the connections.

Step 1: Understand the problem#

The ratio is the requirement here. Get the read and write numbers on the board in the first two minutes and half the design argues itself.

You askThey sayWhat it settles
How many concurrent viewers, and how many writers?Tens of millions watching. One scorer.Everything. One write to a hundred million reads flips the usual instincts: the database is trivial and the connection layer is the entire problem.
How fresh does the score have to be?Within a second or two of the ball.Whether you can poll. A two second budget makes a cached snapshot good enough as a fallback, which is what saves you when connections cannot be held.
Does everyone have to see the same score at the same instant?No, but it must never go backwards.A version number on every update, and a phone that drops anything older than what it already has. The rule is "never backwards", not "everyone at once".
What does the traffic look like over a match?Flat, then a hundred times higher in the last over.The spike is the workload, and it is partly predictable. Capacity is planned for the last over of a final, not for the average Tuesday.
What are people watching on?Mostly phones, on mobile networks, moving between towers.Connections drop constantly. Reconnecting and catching up has to be a designed path, not an error case, and it has to be cheap because millions do it at once.
Is a correction ever published?Yes. Umpire reviews change what already happened.An update is "here is the current state", not "add this to what you had". A phone has to be able to accept "that six was actually a four".

What you are building#

Functional requirements
What the system does.
  • Live score, wickets, overs and the current ball. A small document, a few hundred bytes, updated a few times a minute.
  • Deliver an update within about a second of the event. Later than the crowd noise next door and the product has failed.
  • Handle tens of millions of concurrent viewers. Concurrent, and arriving in a spike when a big wicket falls.
  • Correct a score after a review. Scores go backwards sometimes, and a design that only ever adds breaks.
Non-functional requirements
How well it has to do it. The numbers you commit to.
  • Peak 30 million concurrent viewers, reached within minutes.
  • Under 1 second from scorer input to a phone screen at p95 (95 out of 100 updates arrive faster than this).
  • A viewer must never see the score go backwards except for a real correction.
  • Cost per viewer matters. This runs for six weeks a year and idles the rest.
Out of scope
What you cut, and say so out loud.
  • Video streaming. Several orders of magnitude more bandwidth and a completely different design.
  • Commentary text and social feeds. Same delivery layer, much larger payloads.
  • Betting odds, which have latency and correctness requirements that would take over the discussion.

Back of the envelope#

This is the page where the napkin math genuinely decides the design rather than confirming it. (New to this kind of estimate? The back of the envelope chapter shows the method.)

Do it once by hand with the default numbers. If 30 million phones poll every 5 seconds, that is 30,000,000 / 5 = 6 million requests a second. Each request and reply carries about 600 bytes of HTTP headers around a 400 byte score, so about 1,000 bytes each, and 6 million of those a second is 48 gigabits a second. Pushing instead, the score changes about once every 30 seconds, so each phone gets one 400 byte message every 30 seconds: 1 million messages a second across everyone, about 3.2 gigabits a second. Polling costs 15 times more, to deliver the same score.

Fan out cost
30 million
5 seconds
400 bytes
Polling: requests per second30M / 5s = 6,000,000
Polling: bandwidthpayload + ~600B of headers = 48 Gbps
Push: updates per secondone event per 30s = 1,000,000 sent
Push: bandwidth3.2 Gbps
Push: connection servers at 100k each300
15x
more bandwidth for polling than for pushing

Polling asks 6,000,000 times a second for a value that changes twice a minute, and each ask carries more header than payload. Push sends only when something happens. The catch is the other row: push needs about 300 servers holding connections open, and those servers exist whether or not anything is happening.

Drag the poll interval down to two seconds and watch the multiplier. Then remember that during the last over, people pull down to refresh by hand, which is polling with no interval at all.

Step 2: Propose the high level design#

The core entities
  • MatchOne row of current state: runs, wickets, over and the rest of the scorecard, with a version.
  • UpdateThe full state at a new version. Never a delta, so a missed update fixes itself with the next one.
  • ConnectionOne phone's server sent events stream to an edge server, about 100,000 per server.
  • VersionGoes up by one with every update, corrections included. Phones drop anything older than what they hold.
Never breaks: A phone never shows an older version after a newer one.

Push or poll#

There are four ways to get news from a server to a browser or app: plain polling, long polling, server sent events and WebSockets. WebSockets vs SSE vs long polling walks through all four, mostly with a cricket score as the example. Here is how the choice plays out at 30 million viewers.

How does the score reach 30 million phones?

  1. First try
    Every phone asks our score API every 5 seconds
    Stateless servers, a database read per request, nothing clever. It is how most apps start.
    Where it breaks: That is 6 million requests a second at our own servers, for a value that changed twice a minute. And a 5 second poll means a wicket can show up 5 seconds late.
  2. Better
    Poll through a CDN with a 2 second cache
    A CDN is a network of servers spread around the country that keep copies of files close to users. Phones ask the CDN, and each CDN location asks us at most once every 2 seconds. Our servers barely notice the crowd.
    Where it breaks: Our servers are safe, but the bandwidth bill is not: every poll still pays about 600 bytes of headers, all match long, whether or not a ball has been bowled. And the score is still a few seconds late.
  3. What you ship
    Push to engaged viewers, CDN polling for everyone else
    Viewers actively watching the screen hold an open connection and get each ball pushed within a second. Everyone else polls the CDN snapshot. When the push tier is full or broken, viewers fall back to polling instead of seeing nothing.

Here are the three shapes side by side.

Delivery
Stateless, scales without effort, and it survives anything. A short CDN cache collapses 30 million requests into a handful of origin fetches, so the origin barely notices. The cost is bandwidth at the edge and latency: a 5 second poll means up to 5 seconds of staleness, and the wicket that everyone reacts to arrives late.
The number that decides it

A poll costs roughly 600 bytes of request and response headers to deliver 400 bytes that usually have not changed. At 30 million viewers on a 5 second interval, that overhead alone is most of your bandwidth bill, and it is paid continuously whether or not a ball has been bowled. Push pays per event. The crossover is not close.

The API#

Three endpoints. Two words first: a TTL (time to live) is how long a cache may keep a copy before asking again, and server sent events (SSE) is one long HTTP response that never finishes, down which the server writes a new line whenever there is news.

GET/v1/matches/{id}/score
returns 200 { version: 214, runs: 178, wickets: 4, over: "18.4" } with Cache-Control: max-age=2
Why: The polling fallback, and it is a plain cacheable GET on purpose. A 2 second CDN cache turns tens of millions of requests into a handful of origin fetches, and this path has to keep working when the push tier is refusing connections.
GET/v1/matches/{id}/stream
returns 200 text/event-stream
Why: Server sent events rather than WebSocket. The traffic only flows one way, SSE reconnects automatically in the browser, and it is plain HTTP so it survives proxies that block the WebSocket upgrade. Choosing the simpler transport because the data only flows one way is worth saying out loud.
GET/v1/matches/{id}/stream?since=213
returns 200, current state first
Why: On reconnect the client sends the last version it holds and immediately receives the current state, not a replay of what it missed. Since every update is full state, there is nothing to replay and no per viewer position for the server to remember.

The data model#

One table, and it is tiny. Ingest is its only writer, and every update replaces the whole row with a higher version.

match_stateone row per match, written only by ingest
match_idbigintPKThe topic on the update bus carries the same id, so one match is one row and one topic.
versionbigintGoes up by one with every update, corrections included. Phones drop anything lower than what they already hold.
runs, wicketsint, intTotals, never a "+6". A phone that missed three updates is right again after the next one.
overvarchar(5)"18.4" is eighteen overs and four balls.
detailjsonEverything else the score screen shows. Also full state, so it is always sent whole.
updated_attimestampIngest time. Useful for alerts on a match that stopped updating, never for ordering.
Sample row
88412 | 214 | 178, 4 | 18.4 | {...} | 21:42:07
A whole season is a few thousand rows taking a few writes a minute. This is the least interesting store on the page, and that is the point: all the work is in moving one row to thirty million phones.

The whole system on one whiteboard#

Figure 1 is the same system as the picture at the top, laid out flat. The update bus is a pub/sub system: the publisher sends a message once to a topic, and every subscriber of that topic gets a copy (queue vs pub/sub vs event stream explains the difference from a work queue).

Figure 1. One event multiplies twice: once across a few hundred edge servers, then once more inside each of them. Doing it in that order is the difference between moving a few hundred messages across the network and moving thirty million.

Walking Figure 1:

  1. One person, one keystroke. The entire write path of this system is a rounding error.
  2. Ingest writes the new state to the match store and stamps it with a version. Versions are what make corrections and reconnects work later.
  3. It publishes to the bus topic for this match. Servers for a match nobody is watching receive nothing.
  4. The snapshot builder rebuilds a small file of the current state.
  5. That file is published to the CDN with a 2 second TTL. It is the fallback for anyone who cannot hold a connection. (A CDN is the second layer in caching patterns, and that chapter is the prerequisite for this one.)
  6. The bus delivers one message to each edge server. A few hundred messages, total.
  7. Inside each edge server, that one message becomes 100,000 socket writes from a buffer that was serialised (turned into bytes ready to send) exactly once.

Two supporting boxes sit off the main path. The connection LB (load balancer) decides which edge server a phone connects to. It is sticky, so a phone keeps the same server for the life of its connection (load balancing explains why connection holding servers want this), and it drains slowly: when a server is being taken out, it stops sending new phones there and lets the existing ones leave over time. The catch up API serves phones that reconnect: they send the last version they saw and get the current state straight away, with nothing to replay, which keeps a stadium full of phones switching towers affordable.

The shape to notice is that the multiplication happens as late as possible. One event reaches a few hundred edge servers, and only inside those servers does it become tens of millions of messages. Fanning out any earlier means moving that volume across the network instead of within one process.

Your answer

Step 7 turns one message into 100,000 socket writes on one machine. Roughly how much CPU should that cost per ball, and what is the naive implementation that gets it badly wrong?

Step 3: Design deep dive#

What the edge server actually does#

At 100,000 connections on one machine, every ball means 100,000 writes. A socket is one open connection to one phone, and each socket has a send buffer: a small queue of bytes waiting to go out. If a phone's network is slow, its buffer fills up.

How does one edge server send a ball to its 100,000 phones?

  1. First try
    Build the message per phone, write, wait, next
    Loop over the connections. For each one, build the JSON, compress it, write it, and wait until it is sent before moving on.
    Where it breaks: The same score gets serialised and compressed 100,000 times for an identical result. Worse, one phone in a lift with no signal makes the loop wait, and every phone after it in the list waits too.
  2. Better
    Serialise once, still wait on each write
    Build and compress the bytes a single time, then reuse that one buffer for every connection. The repeated work is gone.
    Where it breaks: The waiting is not. One slow phone still stalls delivery for everyone else on that machine, and on mobile networks there is always a slow phone.
  3. What you ship
    Serialise once, never wait, drop for slow phones
    Write the shared buffer to every socket without waiting. If a phone's send buffer is already full, skip it for this update. That is safe because every update carries the full current score, so the next one fixes that phone.

Here is the third rung, one message at a time.

1/5 One message arrives per event, not per viewer. Every edge server subscribes to the same match topic, so the bus sends a few hundred messages rather than tens of millions.

That last point is the design decision that makes everything else possible. Send state, not changes. A delta is a message that says what changed ("+4"). A state message says what the score is now ("155"). A viewer who misses a state message is not broken, they are briefly one ball behind, and the next message fixes them with no replay, no acknowledgement, and no per viewer bookkeeping.

Try it. This phone is on a bad network: it never receives ball 2 or ball 6, and a retried copy of ball 4 arrives late, after ball 5. Switch what the server sends.

One phone, a bad network, eight balls
Phone receivesPhone showsReal score
ball 1: +1151151
ball 3: +0151155
ball 4: +6157161
ball 5: +1158162
ball 4: +6late copy164162
ball 7: +2166164
ball 8: +4170168
After ball 8 the phone shows 170. The real score is 168.
The phone added up what it was told. It missed the 4 off ball 2, then counted the 6 off ball 4 twice when the retry arrived. It finishes on 170 when the real score is 168, and no later message can fix that, because every message only says "add this much".
Made up numbers. The phone never receives ball 2 or ball 6, and ball 4 arrives twice. The real score after ball 8 is 168.

With "+runs" the phone ends up 2 runs wrong and stays wrong for the rest of the match. With the full score it heals, but the late copy pulls the score backwards for a moment. Only the third option gets both right, which is where the third deep dive picks up.

The spike is the workload#

Traffic here is not steady. A wicket falls, everyone opens the app, and connections arrive in a wall.

Worse is the reconnect wave. If an edge server dies holding 100,000 connections, all of them try to reconnect at once.

An edge server dies holding 100,000 connections. What should those phones do?

  1. First try
    Reconnect after a fixed delay, say one second
    Simple, and each phone is back quickly.
    Where it breaks: They all lost the connection at the same moment, so they all come back at the same moment. 100,000 connection attempts land together on servers already near capacity, push them over, and those drop their own phones. Each wave is bigger than the last.
  2. Better
    Wait a random time before reconnecting
    Each phone waits a random time, and the possible wait grows after every failed attempt. This is backoff with jitter, and it spreads the wave over minutes instead of one second.
    Where it breaks: It spreads the wave, but a server near its limit still accepts every phone that arrives and then serves all of them badly. And a big wicket brings a wall of brand new connections that no backoff controls.
  3. What you ship
    Jitter, plus refuse early, plus fall back to polling
    Random backoff on the phone. A server near capacity refuses new connections immediately and points them elsewhere. A phone that cannot get a connection reads the CDN snapshot instead, so it gets a score a couple of seconds late rather than no score.

The three defences, in plain words:

Randomised backoff on the client, which spreads a reconnect wave over minutes instead of seconds. The highest value code in this system lives on the phone. Retries, backoff and circuit breakers shows why the randomness matters more than the delay.

Admission control at the edge. When a server is near capacity, refuse new connections immediately with a pointer elsewhere rather than accepting them and degrading everyone. This is load shedding: choosing what fails before overload chooses for you, covered in rate limiting vs throttling vs backpressure.

Fall back to polling. If the push tier cannot take a viewer, serve them the CDN snapshot. They get a score a few seconds late instead of no score at all, and the CDN capacity is already there for the fallback path.

The score that goes backwards#

Third umpire overturns a decision. The score you already delivered is wrong.

Every update carries a version from the ingest service that only ever goes up, and phones ignore anything with a version lower than what they hold. That alone fixes the common ordering problem, where a retried or delayed message arrives after a newer one: it is the late copy of ball 4 in the widget above. The promise "your view never goes backwards" has a name, monotonic reads, and it is one of the cheaper promises in consistency models.

A correction is a new update with a higher version and a lower score. Say the phone holds version 214 with 178 runs, and the replay shows the ball bounced before the rope, so the six was really a four. Ingest publishes version 215 with 176 runs. The version went up, so every phone accepts it, even though the score went down. That is exactly why the version is a separate field and not the score itself. A phone that treats "score only goes up" as a rule will show a stale score forever after the first review, and it will be a support ticket nobody can reproduce.

Break it#

Match traffic
group stage
group stagefinal, 30Mwicket fallsserver diesdefended
Healthy. 4 million viewers on 50 edge servers. One event becomes 50 messages on the bus and 4 million socket writes spread across the fleet. Comfortable.
Read every step as text
  1. group stage · Healthy. 4 million viewers on 50 edge servers. One event becomes 50 messages on the bus and 4 million socket writes spread across the fleet. Comfortable.
  2. final, 30M · Degraded. 300 servers at close to 100,000 connections each. Memory per connection is now the limit, not CPU and not bandwidth. Each connection costs a few kilobytes of buffers and that is what fills the machine.
  3. wicket falls · Degraded. A million people open the app within twenty seconds. These are new connections, and setting one up costs far more than delivering an update down an existing one. The connect rate, not the message rate, is what hurts.
  4. server dies · Down. 100,000 connections drop and every one of them retries immediately, because they all use the same fixed delay. The wave lands on servers already near capacity and pushes them over, which drops more connections and produces a bigger wave.
  5. defended · Healthy. Randomised backoff spreads the reconnects over minutes. Servers near capacity refuse new connections rather than accepting and degrading, and refused viewers fall back to the CDN snapshot. Some people get a score two seconds late. Nobody gets no score, and there is no cascade.

Trade-offs#

ChoiceWhat you gainWhat you payPick it when
Push over pollingSub second delivery, and bandwidth that grows with events rather than with viewers times poll rate.A fleet of servers holding connections, sized for peak, that idles between matches.Latency is part of the product. For a score during a final, it is the product.
Full state per update over deltasA dropped message heals itself, there is nothing to replay, and the server keeps no per viewer position.Slightly larger payloads, which is nothing when the state is a few hundred bytes.Small state that changes wholesale. Deltas only pay off for large documents.
Server sent events over WebSocketSimpler, plain HTTP, automatic browser reconnect, and it passes through proxies that block upgrades.One directional, so anything the client needs to send takes a separate request.The data genuinely flows one way, which for scores it does.
CDN polling as a fallback tierThe push tier can shed load into it instead of failing, and it costs nothing when unused.Two delivery paths to build and keep consistent.Any system where peak is many times the average and degrading gracefully is worth more than uniform latency.

The level bar#

One moment from this round, answered three ways. Pick the one you think is staff level before the labels appear. (What changes at senior and staff explains the gap.)

Pick the staff level answer

InterviewerThe ads team wants a personalised message attached to every ball update. Fine?

Show which is which without guessing
  • Answer A: Mid level. Every update now has to be built per connection, 100,000 times per ball on each edge server, which is the exact cost the design avoided.
  • Answer B: Senior. Protects the hot path. Does not say how the separate path survives the same spike.
  • Answer C: Staff. Keeps the property that makes the fan out cheap, and designs the new feature so it cannot create its own stampede.

Then run the question cold, with a 45 minute timer and no notes, and score the run on the five things this question is really testing.

Score your last run
0 / 10
  1. Push or poll
  2. Where it multiplies
  3. Inside the edge
  4. Ordering
  5. An edge server dies
Pick 0, 1 or 2 on every row. Be the strict interviewer, not the kind friend.

Interview replay#

Interviewer
30 million people watching one match. Push or poll?
They want the reasoning, not the answer. Both answers can be right if the reasoning is.
You
Push for the engaged viewers, with polling through a CDN as the default and the fallback. The score changes about twice a minute, so polling every five seconds asks about twenty billion times an hour for a value that changed a hundred or so times, and each ask carries more header than payload. Push sends only on events. But push needs a stateful fleet sized for peak concurrency, so I would keep the polling path so I can shed load into it rather than fail.
Gives the ratio that makes the decision, then immediately names what push costs. Both halves matter.
Interviewer
One edge server holds 100,000 connections. What does it do when a ball is bowled?
Checking whether the fan out is understood at the level of what the machine actually executes.
You
It receives one message from the bus, serialises and compresses it once, then writes those same bytes to every socket. Serialising per connection would be doing identical work a hundred thousand times. Connections that cannot keep up get the update dropped rather than blocking the loop, because one slow phone must not stall delivery for everyone else on that machine.
Serialise once and never block on a slow consumer are the two things that make this machine work. Both are concrete.
Interviewer
Is dropping updates acceptable?
Testing whether the earlier answer was considered or careless.
You
Here, yes, because every update carries the full current score rather than a delta. A viewer who misses one is a ball behind until the next update arrives, which is seconds later. That is the reason to send state rather than events: it makes loss self correcting and it means the server keeps no per viewer position at all.
Justifies it from a design decision made earlier rather than shrugging. This is the strongest exchange available in this question.
Interviewer
An edge server dies. Walk me through the next sixty seconds.
The reconnect storm, which is the failure that actually happens.
You
100,000 connections drop and try to reconnect. If they all use the same delay they arrive together, land on servers already near capacity and can cascade. So: randomised backoff on the client to spread them over minutes, admission control so a near full server refuses immediately instead of accepting and degrading everyone, and refused viewers fall back to the CDN snapshot. They get a score two seconds late rather than nothing.
Three defences at three layers, and the degradation path is named rather than implied.
Interviewer
The third umpire reverses a decision and the score drops. Any problem?
A domain specific trap that catches people who assumed the score only goes up.
You
Only if the client assumed the score only goes up. Every update carries an increasing version from ingest and clients ignore anything older than what they hold, which handles out of order and retried messages. A correction is a higher version with a lower score. That is exactly why the version is a separate field rather than being derived from the score.
Answers the trap and explains why the design already handled it, which is better than proposing a fix.

Checkpoint#

Checkpoint

1. Why is polling so much more expensive than push here?

2. Why send the full score in every update rather than a delta?

3. An edge server holding 100,000 connections dies. What is the danger?

Worth memorising
  • One writer, tens of millions of readers. Polling costs an order of magnitude more bandwidth than pushing: about 15 times at a 5 second poll.
  • 100,000 connections per edge server, so one message becomes 100,000 socket writes inside one process.
  • Serialise the payload once per server, not once per connection. Otherwise the same bytes are built 100,000 times per ball.
  • Version every update. Clients drop anything older than what they already have, so the score never goes backwards.
Say this in 60 seconds

The defining ratio is one write to roughly a hundred million reads, so this is a fan out problem and not a storage problem. I would push over server sent events to engaged viewers and keep CDN backed polling as both the default for casual viewers and the fallback under stress, because polling asks constantly for a value that changes twice a minute and pays more in headers than payload. One event goes to a few hundred edge servers, and only inside each server does it become a hundred thousand socket writes, so the multiplication happens as late as possible. Each server serialises once and reuses the bytes, and drops updates for connections that cannot keep up rather than blocking, which is safe because every update carries full state rather than a delta, so a missed message self corrects. Updates carry an increasing version so a review that lowers the score still moves forward. And the failure I would design for is an edge server dying: a hundred thousand synchronised reconnects, handled with randomised client backoff, admission control, and falling back to the polling path rather than refusing service.