Dev Skills
← Live score push·Worked solution

Live score push — a full worked answer

The whole thing, start to finish, in the order you would actually say it. Read it to learn the shape of a strong answer — then go and produce one yourself, because reading is not the same skill as producing.

core
Common trap
Read this second, not first. Reading a good answer feels like learning and only builds recognition. If you have not tried this problem yet, close this page and write your own answer first. It will feel worse and it is the part that works.

The question

Design a service that pushes live sports scores to millions of people watching the same match at the same time. A score update should reach everyone within a second or two.

What kind of problem this is

Live push. The server has to speak first, to a lot of people, at the same time.

Capacity is measured in concurrent connections, not requests per second, and the hard part is the fan-out layer, not the socket.

Before anything: where is the slack?

How long the system has before a person notices. Ask this before you draw a single box — it decides more of the design than scale does.

Delivery: 1–2 seconds. Connection setup: seconds. Reconnect: seconds.

A second or two of slack, bounded by something outside your system: the person in the next room shouting at their television.

The physical world sets this delivery budget, not engineering taste. If a neighbour with a TV feed shouts before your app updates, the product has failed in a way users notice viscerally. Broadcast feeds themselves run several seconds behind live, which gives you a little room, but not much. Call it one to two seconds end to end.

That is more slack than it first appears, and it is worth spending on purpose. It means you do not need microsecond delivery, you can batch updates into small windows, and you can accept a message taking a couple of hundred milliseconds to traverse a fan-out layer.

Connection setup has seconds of slack. A short pause when the app opens is fine. This matters more than it sounds, because it is what lets you stagger reconnections after a deploy instead of accepting a million simultaneous handshakes.

Historical data, statistics and commentary have minutes of slack and should be plain cached HTTP requests, not pushed. Mixing them into the live path is an easy way to over-build this design.

So: One to two seconds means you can batch updates into ~250 ms windows, which cuts fan-out work dramatically for a delay too short to notice. It also means the live channel should carry only what is actually live. Everything else is a cacheable request.

1

Requirements

about 5 min

Why this stage exists: Because the question is deliberately vague, and every box you draw before this is a guess.

What you are being asked for: State your assumptions, ask one or two questions that change a box, and propose the scope.
What a strong answer sounds like

Assumptions: global, mobile-first. Traffic is spiky but predictable: quiet for hours, then ten million people connect for one final. Updates per match are infrequent: a football match has a few dozen meaningful events across ninety minutes, not thousands. Score data itself is tiny, a few hundred bytes.

First question, which decides whether this is easy or hard: does everyone watching a match see identical data, or is it personalised, with their team highlighted, their bets, their notifications? Identical data means one message fans out unchanged to millions and can be cached and broadcast; personalised means a per-user computation for every update, which is a completely different system. I will assume identical per match, with personalisation layered on the client.

Second question: is the score feed something we own, or does it come from an external provider? An external provider means an ingestion path with deduplication, corrections and their outages. Provider corrections (a goal disallowed after review) are a real requirement. I will assume external providers.

Scope: ingest score events, push them to everyone watching a match, and handle reconnection and catch-up. Out of scope: video streaming, betting, and the statistics pages, which are ordinary cached reads.

The costs named here
Identical payload per match, personalisation on the client → costs: Some features that would be easier server-side move to the client. In exchange one message serves millions of viewers unchanged, which is what makes this affordable.
Say this
"Assuming everyone watching a match gets the same payload, with personalisation on the client. The design depends on that assumption. Identical data means one message fans out unchanged to ten million people; personalised means ten million computations per goal. And the feed comes from an external provider, so I need to handle corrections."
Common trap
Assuming personalised payloads without saying so, then designing a per-user pipeline for data that is identical for everyone. State the assumption; it is the biggest lever in this problem.
What an interviewer is ticking off in this stage
  • Described the traffic as spiky and predictable, not steady
  • Noted that updates per match are infrequent but audience is enormous
  • Asked identical-versus-personalised and named the consequence
  • Asked where the feed comes from, and raised corrections as a requirement
  • Proposed scope and excluded statistics and video
2

Actors and lifecycle

about 6 min

Why this stage exists: Because the happy path is the easy half. The failure branches are where the design actually lives.

What you are being asked for: List the actors — including the system — then write the life of a score event as a chain, and add the failure branches.
What a strong answer sounds like

Actors: the viewer, the data provider, and the system, which decides what is a duplicate, what is a correction, which connections care about which match, and what a reconnecting client has missed.

The chain: event received from provider → validated and deduplicated → stored as the current match state → published to the fan-out layer → delivered to connected clients → rendered.

Failure branches. At received: the provider sends the same event twice, so every event needs a provider event id and deduplication. Otherwise the score goes to 2-1 and then 3-1. At received: the provider sends a correction, a goal disallowed after review. This is the branch that makes the problem interesting: it means updates are not append-only, so clients must be able to handle a value going backwards, and I should push full current state rather than increments. Pushing "score is now 1-1" is idempotent and self-correcting; pushing "add one goal" is not, and any missed or duplicated message corrupts the display permanently. That is a design decision, and it belongs in the lifecycle.

At received: the provider goes down entirely. The app should show the last known state with a timestamp rather than a blank or a stale-looking score presented as live. At published: the fan-out layer is behind, so the score is late; because the payload is full state, a late message is still correct, just late.

At delivered: the client is disconnected (a tunnel, or the app backgrounded). They must reconnect and catch up, and because we push full current state, catching up means fetching the current state, which is the same cached endpoint everyone else uses. That is a large simplification and it falls directly out of the full-state decision.

At delivered: a deploy restarts every server, so millions reconnect at once. Without jittered backoff this is a self-inflicted denial of service that is worse than the traffic being served.

One branch the system handles on its own: when a match ends, ten million connections should be released rather than held open for hours by clients that have stopped caring.

Common trap
Designing incremental updates ("goal scored, add one"). One missed message and the client is permanently wrong, with no way to notice. Full current state makes every message idempotent and makes catch-up free.
What an interviewer is ticking off in this stage
  • Named the system as an actor with real decisions
  • Wrote the event lifecycle from provider to phone
  • Handled duplicate events from the provider with an event id
  • Handled corrections, and chose full-state pushes because of them
  • Handled provider outage — last known state with a timestamp
  • Handled client reconnect and catch-up, simplified by full-state
  • Handled the deploy reconnection storm
  • Handled connection cleanup when a match ends

The lifecycle, drawn

ReceivedproviderDeduplicatedsystemState updatedsystemPublishedfan-outDeliveredconnection serverDuplicate eventdeduplicate on provider event idProvider goes downshow last known state with a visible timestampCorrection — goal disallowedpush full current state, so it self-healsClient in a tunnelreconnect with last-event-id, fetch current state to catch upDeploy drops everyoneslow roll plus jittered client backoffSlow client fills its bufferbounded buffer — drop the client, never the server
A score event, provider to phone. The correction branch forces full-state pushes, and that one decision simplifies three other branches.
3

Numbers

about 6 min

Why this stage exists: Because one part of this is hard and the rest is easy, and they look identical until you do the arithmetic.

What you are being asked for: Estimate concurrent connections, message rate, and total fan-out volume. Then finish "So the hard part here is ___."
What a strong answer sounds like

Concurrency: a major final, 10 million simultaneous viewers. That is the headline number. It counts connections, not requests per second, which is a different capacity model from most problems in this app.

Message rate from the provider: a football match generates maybe 100 meaningful events over 90 minutes, so roughly one event every minute. That input rate is tiny, and noticing it matters.

Fan-out volume: 100 events times 10 million viewers is a billion message deliveries per match. But they are not spread evenly. A goal means 10 million deliveries in one or two seconds, so the instantaneous fan-out rate is around 5 to 10 million messages per second, in bursts, separated by minutes of near silence.

That contrast changes the design. The input rate is one message a minute; the output rate is ten million a second in a burst. There is no ingestion throughput problem at all. The system exists to multiply one tiny message across ten million sockets, quickly, and then be idle.

Connections: at roughly 10 KB of memory per connection including buffers, 10 million connections is about 100 GB spread across the fleet. At, say, 100,000 connections per server that is 100 servers. That is manageable, and it is a memory and file-descriptor problem more than a CPU one.

Bandwidth: 300 bytes per update times 10 million is 3 GB per goal, delivered in a couple of seconds. That is noticeable, and it is why a small payload matters.

So the hard part here is burst fan-out, not ingestion, storage or throughput in the normal sense. And the second hard part is connection management: holding ten million idle connections cheaply and surviving the moment they all reconnect.

Say this
"The provider sends me one message a minute. I have to turn that into ten million deliveries inside two seconds, then go quiet again. So the hard part here is burst fan-out and holding ten million mostly-idle connections. There is no ingestion problem at all, and saying that stops me designing one."
Common trap
Designing a high-throughput ingestion pipeline. The input is one message a minute. Every bit of engineering belongs on the output side.
What an interviewer is ticking off in this stage
  • Stated concurrent connections as the primary capacity number
  • Counted events per match and noticed the input rate is tiny
  • Multiplied to get fan-out volume, and noted it arrives in bursts
  • Estimated memory per connection and derived a server count
  • Estimated burst bandwidth per event
  • Finished the sentence: burst fan-out and connection management

The numbers, drawn

Provider events~1 / minute
Concurrent connections10,000,000
Deliveries in the 2s after a goal~5–10M / sec, in a burst

There is no ingestion problem here at all. So the hard part is burst fan-out and holding ten million mostly-idle connections — including the moment they all reconnect at once.

One message a minute in. Ten million deliveries in two seconds out.
4

High-level design

about 12 min

Why this stage exists: Because now — and only now — you have a reason for every box you are about to draw.

What you are being asked for: Draw the design. Justify each box, and pick a transport with a reason.
What a strong answer sounds like

Transport: server-sent events. Data flows one way, server to client, and SSE gives me automatic reconnection with a last-event-id, which is the catch-up mechanism I need, over plain HTTP that every proxy and load balancer already understands. WebSockets would work but I would be building reconnection and heartbeats myself for a bidirectional channel I do not need. Polling would mean ten million clients asking every two seconds, which is five million requests a second of mostly-unchanged responses. Still, polling a heavily cached endpoint is a reasonable fallback, and it is what I would ship first if I had a week. Justified by the Stage 1 assumption that the client only receives.

Ingestion: a small service receiving provider events, deduplicating by provider event id, applying corrections, and writing the current match state. It stays tiny because the input rate is one message a minute, and I would say out loud that this box does not need to scale.

Fan-out: connection servers each hold a slice of the connections. When match state changes, the update is published once to a pub/sub layer, and every connection server subscribed to that match writes it to its connected sockets. The key decision is that servers subscribe per match rather than per user — so one published message becomes 100 server deliveries rather than 10 million pub/sub messages. So the multiplication happens at the last hop, on the machine that already holds the sockets.

Connection routing: clients are routed to connection servers by match, using consistent hashing, so viewers of the same match cluster onto the same servers. That maximises the value of each pub/sub delivery and keeps subscription counts low. Cost: a very popular match concentrates load onto its servers, so hot matches need to be spread across several, which means a small amount of routing intelligence rather than a pure hash.

Current state cache: the current state of every live match sits in a cache and is also served over a plain cached HTTP endpoint. This does triple duty: it is what a newly connected client fetches to initialise, what a reconnecting client fetches to catch up, and the fallback if push fails entirely. Justified by the reconnection branch in Stage 2, and it is the highest-leverage box in the design because everyone wants identical bytes.

Batching: updates are collected into ~250 ms windows before fan-out. At one event a minute that rarely matters, but during a chaotic period (a goal, then a VAR check, then a correction) it collapses several updates into one delivery. Justified directly by the one-to-two-second slack budget: 250 ms is invisible to a human and it cuts fan-out work meaningfully.

The costs named here
SSE over WebSockets → costs: One-directional, so any future client-to-server feature needs a separate channel. Buys automatic reconnect and plain-HTTP compatibility.
Routing connections by match → costs: A hugely popular match concentrates onto specific servers, so hot matches need spreading. Without it, every server subscribes to every match.
250 ms batching window → costs: A quarter second of added delay, which no human perceives, in exchange for collapsing bursts of updates into single deliveries.
Common trap
Publishing one pub/sub message per connected user. Ten million pub/sub messages per goal will crush any broker. Publish once per match; multiply on the machine that already holds the sockets.
What an interviewer is ticking off in this stage
  • Chose a transport and rejected the other two with reasons
  • Noted polling against a cached endpoint is a legitimate simpler fallback
  • Ingestion service kept small on purpose, justified by the input rate
  • Pub/sub subscription is per match, not per user
  • Multiplication happens on the connection server that already holds the sockets
  • Connections routed by match so viewers cluster
  • Current-state cache serves initialisation, catch-up and fallback
  • Batching window justified by the slack budget

The architecture, drawn

on connectScore providerIngest svcdedupe, correctionsMatch statecurrent, cachedPub/subone channel per matchConn server100k socketsConn server100k socketsViewersSSECached HTTPinit + catch-up
One message in, ten million out. The multiplication happens on the connection servers, which already hold the sockets — never in the pub/sub layer.
Authorposts onceFan-outInbox 1Inbox 2Inbox 3
one write inmany writes out — reads later are free
Publish once per match; each connection server multiplies it across the sockets it already holds. Publishing per user would be ten million broker messages per goal.
5

Deep dive and tradeoffs

about 14 min

Why this stage exists: Because naming what a decision costs is the single clearest signal of seniority there is.

What you are being asked for: Go deep on the two hardest parts. Name a cost after every decision.
What a strong answer sounds like

Hard part one: the connection storm. Traffic is not just spiky, it is scheduled: ten million people open the app in the two minutes before kick-off, so the peak is connection establishment, not steady-state delivery. Each connection costs a TLS handshake and an authentication check, and doing ten million of those in 120 seconds is roughly 80,000 per second of the most expensive operation in the system. Defences: terminate TLS at the edge with session resumption so returning clients skip the expensive handshake; keep the authentication check on connect as cheap as possible, ideally validating a token without a database call; and pre-scale, since kick-off time is known in advance, a luxury most systems do not have, so use it. Cost: pre-scaling means paying for idle capacity ahead of the match, which is the correct trade for a scheduled peak.

Deploys during a match are the hardest operational case, because restarting a server drops its connections and they all reconnect at once. Roll slowly, a small percentage of the fleet at a time, and have clients reconnect with exponential backoff and full jitter so the returning herd is spread across tens of seconds instead of arriving together. Better still, do not deploy during a match. That is a legitimate answer, and saying it shows operational judgement rather than avoidance. Cost: a deployment freeze window that constrains the team.

Slow clients are the subtle failure. A client on a bad connection cannot consume messages as fast as you write them, so per-connection buffers grow, and with enough slow clients the server runs out of memory and takes down everyone on it, including the healthy connections. The fix is a bounded buffer per connection, and when it is full, drop the client rather than the server. Because we push full current state, a dropped client reconnects and fetches current state, losing nothing, so the Stage 2 decision pays off again. Cost: users on very bad connections get disconnected more often, and their experience becomes reconnect-and-refresh, which is acceptable and honest.

Hard part two: the hot match. Consistent hashing by match id clusters viewers usefully, but one match can draw more viewers than every other live match combined. Spread a hot match across many connection servers by hashing on match id plus a bucket, so the match becomes N logical channels, each with its own set of servers. Cost: the fan-out layer publishes to N channels instead of one, which is a small multiplier and vastly better than one overloaded server.

Provider failure and corrections, revisited as an operational matter: if the provider stops, the app shows last known state with a visible timestamp. It must never present a stale score as current, because a confidently wrong score is worse than an honest "last updated 3 minutes ago". If a correction arrives, full-state pushes make it self-healing. Cost: a UI requirement, and the discipline to have designed for it.

Multi-region: connection servers run in every region so viewers connect locally, and the score event is replicated to each region's pub/sub layer. The event is tiny, so cross-region replication is cheap and adds around 100 ms, which fits inside the budget. Cost: an extra hop for regions far from ingestion, and slightly different delivery times by region. Users are unlikely to notice at this granularity, but I would state it rather than pretend delivery is uniform.

What I would monitor: connections per server and their distribution, delivery latency from provider receipt to socket write at p99, reconnection rate, buffer-full disconnects, and pub/sub publish latency. A spike in reconnection rate is often the first sign of trouble, before delivery latency moves.

The costs named here
Pre-scaling for a known kick-off → costs: Paying for idle capacity ahead of the peak. Correct for a scheduled event, since the alternative is failing during the only minutes that matter.
Dropping slow clients when buffers fill → costs: Users on bad connections reconnect more often. Prevents a handful of slow clients from taking down every connection on that server.
Cross-region replication of events → costs: ~100 ms extra for distant regions and slightly uneven delivery times. Fits inside a 1–2 second budget comfortably.
Say this
"I push full current state rather than increments. Cost: slightly larger messages. What I get is that every message is idempotent, a correction works automatically, and a reconnecting client just fetches current state from the same cached endpoint everyone else uses — so catch-up needs no special machinery at all."
Common trap
Focusing on message delivery and ignoring connection establishment. The peak here is ten million people connecting in two minutes before kick-off, and a handshake is far more expensive than a message.
What an interviewer is ticking off in this stage
  • Addressed the scheduled connection storm and used its predictability
  • TLS termination and session resumption to cut handshake cost
  • Deploy strategy — slow roll, jittered reconnect, or a freeze window
  • Bounded per-connection buffers, dropping slow clients rather than the server
  • Connected that safely to the full-state push decision
  • Spread a hot match across multiple logical channels
  • Honest degradation when the provider fails
  • Multi-region with the latency budget checked against it
  • Named what to monitor, and the leading indicator
  • Cost named after every decision

The decision this turns on

A
Push full current state
  • —Every message is idempotent — a duplicate changes nothing.
  • —A correction just works; the next message overwrites the wrong value.
  • —Catch-up after a disconnect is simply "fetch current state" — no replay log.
  • —Slightly larger messages, which at 300 bytes is irrelevant.
B
Push increments ("goal scored")
  • —Smallest possible payload.
  • —One missed message and the client is permanently wrong, silently.
  • —Corrections need an explicit undo message and ordering guarantees.
  • —Catch-up needs a replay log with per-client positions.
Which one: Full current state. The payload difference is meaningless at this size, and it removes the need for ordering guarantees, a replay log, and correction handling: three subsystems deleted by one decision. When state is this small, push state.
What you put in the message. This choice decides how hard three other problems are.

And then they push back

A complete answer is where the interview starts, not where it ends. These are the questions this design invites — the interview is decided here far more often than in the diagram.

10x traffic tomorrow. What fails first?
A weak answer

"We would add more servers and scale horizontally." This says nothing. Every system has a first thing to break, and not naming it means you do not know your own design well enough to have found it.

A strong answer

Name a specific component and the reason. "The stateless services scale out fine, so they are not the constraint. The database primary is, because every write goes through one machine and I am at roughly 40% of its write capacity now. So at 4x I am at the ceiling, and I would see write latency climb and connection pool waits before anything errors. The second thing is the cache, because 10x reads on a fixed working set is fine but 10x on a growing key space means my hit rate drops and the misses land on the database that is already the constraint. In order: database writes, then cache hit rate, then the connection pool. And I would find out from p99 write latency, not from errors."

A whole region is down. What do users see?
A weak answer

"We failover to another region." That is the mechanism, not the answer. The question is what a user experiences, for how long, and what is lost.

A strong answer

"I would answer per path rather than globally. Reads: if replicas exist elsewhere, they keep working, possibly stale, and I would say how stale. Writes: if the leader was in the dead region, writes fail until a new leader is promoted. That takes seconds to a minute, and being aggressive about promotion risks promoting during a brief network blip and ending up with two leaders. For contested resources like inventory I would rather be unavailable for that region's writes than risk selling the same thing twice, and I would say that explicitly as a choice. For non-contested writes like posting a comment, take it locally and reconcile. Data loss: with async replication, anything committed in the last few seconds and not yet replicated is gone, so I would use semi-synchronous replication for anything I cannot lose. And DNS-based failover is slow because clients cache DNS for minutes, so I would not rely on it alone."

How do you deploy a new version with no downtime?
A weak answer

"Rolling deploy behind the load balancer." Correct and incomplete. The hard part is database changes and in-flight requests, and stopping there suggests you have only done it on a stateless service.

A strong answer

"Rolling deploy, taking instances out of the pool, draining in-flight requests (skip connection draining and you drop live traffic during a zero-downtime deploy), then updating, waiting for health checks, and putting them back, keeping enough capacity in the pool throughout. The part that makes it work is that the new version must tolerate the old version running beside it, because for several minutes both are live. That means database migrations get split: add the new column, deploy code that writes both and reads the old, backfill, deploy code that reads the new, then remove the old column. That is three or four releases instead of one. Cost: every schema change is now a multi-deploy sequence, which is slower and is the price of not taking downtime. And I would rather roll forward to a known-good build than roll back, because undoing a migration is much harder than undoing code."

Why a queue instead of just calling the service directly?
A weak answer

"Queues decouple services and are more scalable." True in the abstract and it does not explain why this particular call needs one. Queues also add duplicate delivery, ordering questions and debugging difficulty, and not mentioning those means you added one by reflex.

A strong answer

"Because the user does not need to wait for this work to finish, and because the downstream cannot absorb my spikes. Concretely: sending the receipt email takes 800 ms and can fail, and there is no reason to make the customer wait for it or to fail their order because a mail provider is down. The queue absorbs a 10x spike into a longer backlog instead of a broken service. What it costs: the customer gets 'received' rather than 'done', so the product has to be honest about that; every consumer must be idempotent because delivery is at-least-once; and a failure now happens in a worker minutes later, which is harder to debug. I would not queue the stock check or the payment, because the user needs a real yes-or-no answer before they leave the page. A queue there adds latency and buys nothing."

30x traffic for four hours on sale day. Same design?
A weak answer

"We would autoscale." Autoscaling takes minutes and this spike arrives in seconds. Also, the thing that breaks is usually the part that cannot autoscale, which is why naming autoscaling as the whole answer is a tell.

A strong answer

"Mostly the same design, but three things change. First, this spike is predictable (the date is known), so I pre-scale rather than relying on autoscaling, which reacts over minutes while the spike arrives in seconds. Second, the stateless tiers scale out, but the database and any shared counter do not, so I would protect them: a queue or waiting room in front to convert a burst into a rate, and aggressive caching of everything that does not need to be exact. Third, I would decide in advance what to shed (recommendations, analytics, non-essential enrichment) so degradation is a choice rather than a surprise. And I would load test at 30x before the day, because the failure I have not thought of is the one that matters."

Which alert fires first when this system starts failing?
A weak answer

"We would alert on CPU and memory." Those are symptoms of the machine, not of the user experience, and they fire either far too late or constantly for no reason.

A strong answer

"I would alert on user-visible symptoms first and causes second. The first alert should be error rate or latency at p99 on the main user path, because that is what maps to someone being unhappy. Then a small number of leading indicators that fire before users notice: queue age (not depth), because age maps directly to how late the work is; replication lag; cache hit rate dropping, which precedes database saturation; and connection pool wait time, which is the earliest sign of a slow dependency. For this system specifically, the earliest signal is usually cache hit rate, because everything downstream is sized assuming the cache absorbs most reads. I would not page on CPU. It is a dashboard metric, not an alert, because a machine at 90% CPU serving requests fine is not an incident, and a machine at 20% CPU timing out is."

The queue is backed up by an hour. What do you do?
A weak answer

"Add more consumers." Sometimes right, sometimes wrong: if consumers are failing and retrying, adding more accelerates the failure. Not distinguishing the two cases is the tell.

A strong answer

"First I work out which of two things it is, because the fixes are opposite. If consumers are healthy and simply outnumbered, scale them out, and check what they are hitting, because often the ceiling is a database and adding workers just moves the queue there. If consumers are failing and retrying, more consumers makes it worse; I find the poison message or the broken dependency and stop the retry storm. Then I triage rather than drain in order: if urgent and non-urgent work share a queue, the non-urgent is now delaying the urgent, which is a lesson for the design, not just the incident. Then I decide what to drop — an hour-old 'your driver is arriving' notification is worse than useless and I would discard it rather than deliver it. And afterwards I alert on message age and backlog growth rate, not depth, because depth crossing a threshold tells you an hour too late."

What does this cost per month?
A weak answer

"I am not sure, it depends on the provider." Interviewers rarely want exact prices. They want to know whether you understand which line item dominates, and that is answerable from your own estimates.

A strong answer

"I would not guess at prices, but I can tell you the shape, which is what matters. The dominant cost here is egress bandwidth: my estimate said 1,350 GB a second to viewers, and at that volume nothing else on the bill comes close. Second is storage, because I keep raw data for two years and it grows every day whether anyone reads it or not. Third is compute, which is elastic and therefore the easiest to control. That ordering tells me where to spend engineering effort: a percentage point of CDN hit rate is worth more than any amount of compute optimisation, so cache hit rate is the number I would put on a dashboard and defend. Storage cost I would attack with lifecycle policies moving cold data to cheaper tiers and with tiered granularity. Compute I would run on interruptible capacity because the work has hours of slack."

The topics this answer used

Now do it yourself

You have read a good answer. That is worth very little until you can produce one on a blank page — so go and write this problem without looking, and compare afterwards.