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.
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.
one scorertens of millions of phonesabout one write to a hundred million reads
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 ask
They say
What 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".
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.
one poll: 600 bytes of headersaround a 400 byte scorepoll every 5 s48 Gbpspush per ball3.2 Gbps30 million phones: 15 times more, for 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.
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
every 5s1once per 2s2Viewers30MCDNcache 2sScore API
every 5s1once per 2s2Viewers30MCDNcache 2sScore API
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.
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.
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.
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_id
bigint
PK
The topic on the update bus carries the same id, so one match is one row and one topic.
version
bigint
Goes up by one with every update, corrections included. Phones drop anything lower than what they already hold.
runs, wickets
int, int
Totals, never a "+6". A phone that missed three updates is right again after the next one.
over
varchar(5)
"18.4" is eighteen overs and four balls.
detail
json
Everything else the score screen shows. Also full state, so it is always sent whole.
updated_at
timestamp
Ingest 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.
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).
ball event12versioned34publish5to every server6push7where to connectlatest stateScorerone writerIngestvalidate, versionMatch storecurrent stateUpdate busone topic per matchSnapshot buildersmall JSONCatch up APIsince version NEdge servers100k conns eachCDNsnapshot, 2s TTLConnection LBsticky, drains slowlyViewersphones, mostly
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:
One person, one keystroke. The entire write path of this system is a rounding error.
Ingest writes the new state to the match store and stamps it with a version. Versions
are what make corrections and reconnects work later.
It publishes to the bus topic for this match. Servers for a match nobody is watching
receive nothing.
The snapshot builder rebuilds a small file of the current state.
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.)
The bus delivers one message to each edge server. A few hundred messages, total.
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.
one balla few hundred messages100,000 socket writes inside each server
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?
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.
Update busEdge serverViewersball 18.4, version 214serialise oncewrite to 100k socketsdrop slow consumersthey catch up on the next one
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 receives
Phone shows
Real score
ball 1: +1
151
151
ball 3: +0
151
155
ball 4: +6
157
161
ball 5: +1
158
162
ball 4: +6late copy
164
162
ball 7: +2
166
164
ball 8: +4
170
168
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.
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.
all at onceone secondrandom backoffspread over minutesthe most valuable code here lives on the phone
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.
full: refused at once,with a pointer elsewhereCDN snapshot, 2 s TTL:a few seconds latea late score beats no score
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.
version 214178version 215176six was a fourversion up, score down: acceptnever make "the score only goes up" the rule
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
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.
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.
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.
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.
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.
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
Push or poll
Where it multiplies
Inside the edge
Ordering
An edge server dies
Pick 0, 1 or 2 on every row. Be the strict interviewer, not the kind friend.
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.
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.