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.
Live cricket scores / at a glance
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 inverts the usual
instincts: caching is easy here, and the hard part is the connection layer.
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 inverts 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 viable 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 client that drops anything older than what it has. Monotonicity, not simultaneity.
What does the traffic profile 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 cells.
Connections drop constantly. Reconnect and catch 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.
Updates are versioned state, not an append only event stream. A client has to be able to accept "that six was actually a no ball".
This is the page where the napkin math genuinely decides the architecture rather than
confirming it.
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, users refresh manually, which is polling with no interval at all.
every 5s1once per 2s2Viewers30MCDNcache 2sScore API
every 5s1once per 2s2Viewers30MCDNcache 2sScore API
Stateless, trivially scalable, 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.
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 is entirely one directional, SSE reconnects automatically in the browser, and it is plain HTTP so it survives proxies that block upgrades. 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 cursor for the server to remember.
ball event12versioned34publish5to every server6push7where to connectmissed updatesScorerone 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 and stamps it with a version. Versions are what make
corrections and reconnects work later.
It publishes to a bus topic for this match. Servers for a match nobody is watching
receive nothing.
A snapshot of current state is rebuilt and pushed to the CDN with a short TTL, which
is the fallback for anyone who cannot hold a connection.
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 exactly once.
Reconnecting clients ask for everything since the version they last saw, rather than
for the whole state, which keeps a stadium full of cell handovers 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 a process.
Your answer
Step 6 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, the work per event is 100,000 writes. Doing that
naively is the difference between a machine that copes and one that falls over.
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
events. A viewer who misses a message is not broken, they are just briefly one ball behind,
and the next message fixes them with no replay, no acknowledgement, and no per viewer
state to track.
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 reconnect at once. Without randomised backoff on the client, they arrive together at
the remaining servers, push those closer to their limit, and can start a cascade.
Three defences, all cheap:
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.
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.
Degrade to polling. If the push tier cannot take a viewer, serve them the CDN snapshot.
They get a score three seconds late instead of no score at all, and the CDN capacity is
already there for the fallback path.
Third umpire overturns a decision. The score you already delivered is wrong.
Every update carries a monotonically increasing version from the ingest service, and
clients 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.
A correction is a new update with a higher version and a lower score. The version keeps
increasing even though the score decreases, which is exactly why the version is a separate
field and not the score itself. Clients that treat "score only goes up" as an invariant
will show a stale score forever after the first review, and it will be a support ticket
nobody can reproduce.
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 binding constraint, 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 establishing one costs far more than delivering an update to 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.
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 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 monotonicity.
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.
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. This is the difference between 2% and 100% CPU.
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 degrading to the polling path rather than refusing service.