Backpressure and Scaling: Advanced
What you will be able to do
One question, many disguises
- Black Friday brings 5x normal order volume. Walk me through the pipeline.
- Calls to the fraud-scoring service now take 40 ms instead of 4. What happens upstream?
- We are onboarding a customer who triples our event count next month.
- The consumers were down for 2 hours. When is the dashboard current again?
- Lag alerts fire every evening around 6 pm and clear by 9 pm. Why, and what would you change?
The running example: trip events
Events move left to right; each enrichment consumer makes 1 profile lookup per event before handing it to the writers.
| Stage | Capacity (events/s) | Peak today: 40,000/s | Doubled: 80,000/s |
|---|---|---|---|
| Ingest gateway | 150,000 | 27% | 53% |
| Queue writes (24 partitions at 5 MB/s) | 120,000 | 33% | 67% |
| Enrichment consumers (24 at 2,500/s) | 60,000 | 67% | 133% |
| Profile service quota | 90,000 | 44% | 89% |
| Table writers (6 at 15,000/s) | 90,000 | 44% | 89% |
What breaking looks like in a pull pipeline
- Add consumers: the 25th consumer on a 24-partition queue gets no partition and sits idle.
- Bigger machines: each consumer waits on 4 ms lookups, so more CPU adds no throughput.
- Autoscale on CPU: CPU stays low while lag climbs, so the rule never fires.
- The queue will absorb it: a sustained deficit grows the backlog for as long as the peak lasts.
- The consumers break first at 133%; raise partitions and consumers to 48.
- After that fix, the profile service and writers sit at 89% and cap recovery.
- A 3-hour doubled peak with no fix leaves the table 45 minutes stale.
- If capacity runs out anyway, sample location pings and keep every trip lifecycle event.
- ▸Demand: the peak rate, its shape, and how long it lasts.
- ▸Capacity: every stage's rate, and the minimum across them.
- ▸Failure mode: lag, data loss, memory exhaustion, or a cascade into a shared dependency.
- ▸Fix and its next ceiling: what you change, what breaks after it, and what it costs to run.
Every number in the example is an assumption chosen to make the arithmetic visible. In an interview, state your assumptions aloud and let the interviewer correct them.
Where excess load can go
| Mechanism | Where the excess waits | What it costs | Fits |
|---|---|---|---|
| Block | In the upstream caller, which stalls | Latency upstream; a stalled caller can stall its own caller | Hops inside one service or job |
| Buffer | On disk in a durable queue, up to retention | Freshness, plus storage for the backlog | Events that must arrive but may arrive late |
| Shed | Nowhere: the event is dropped or sampled | Completeness | Telemetry where a sample still answers the question |
| Throttle | In the client, which retries later | Client logic; retries can arrive in waves | External producers you can tell to back off |
The unbounded buffer inside a consumer
At 9,500 waiting records, fetch_loop pauses its partitions but keeps calling poll, which returns nothing yet proves the consumer alive. A loop that blocked would stop polling, and past the group's maximum poll interval the consumer is ejected and its uncommitted records replayed elsewhere. Unread records stay durable in the partition, memory stays near 10 MB per consumer, and the slowdown becomes lag. Commit offsets only after the writer accepts a record. With 10 threads finishing out of order, commit the lowest offset not yet written, so a crash replays work and loses none.
Choosing a mechanism per event class
- A 50% sample still traces the route on a map.
- Value decays fast: a position from 30 minutes ago is useless.
- Shed at the gateway, before the event costs a lookup.
- Shedding half removes 24,000 events per second at doubled peak.
- Every event moves money: a lost fare event is a wrong payout.
- Value survives delay: an earnings row 20 minutes late is still correct.
- Buffer in the queue and never shed.
- Order matters per trip, which shapes the partition key.
- Bound every in-memory buffer by count or bytes
- Commit offsets after the write succeeds
- Decide shed or buffer per event class, in advance
- Shed at the earliest stage, before work is spent
- Let a fetch loop outrun the threads that drain it
- Shed events that drive money or state
- Retry throttled calls on a fixed schedule
- Treat a durable queue as unlimited: retention is the bound
Sizing the partition count
| Partitions | Consumer ceiling (events/s) | Utilization at 80,000/s | Surplus for draining |
|---|---|---|---|
| 24 | 60,000 | 133% | None: lag grows 20,000/s |
| 32 | 80,000 | 100% | None: any blip becomes lag |
| 48 | 120,000 | 67% | 40,000/s, capped at 10,000/s downstream |
| 64 | 160,000 | 50% | 80,000/s, capped at 10,000/s downstream |
Repartitioning breaks per-key ordering
The producer picks a partition with hash(trip_id) mod N. Change N from 24 to 48 and a key that lived in partition p now lands in p or p + 24, so about half of all trips move. Events a trip produced before the change sit in the old partition; events after it go to the new one. Nothing forces the old and new partitions to be read in order.
| Event for trip t-9031 | Produced | Partition | Processed |
|---|---|---|---|
| `trip_requested` | 18:02:10 | 7 of 24 | 18:05:10 |
| `trip_accepted` | 18:02:14 | 7 of 24 | 18:05:14 |
| Partition count raised to 48 at 18:02:30 | |||
| `trip_started` | 18:02:40 | 31 of 48 | 18:02:41 |
- Size for growth up frontCheapestCreate 96 partitions when 48 are needed today. The next doubling then needs more consumers and no repartition.
- Drain, then switchNeeds a pauseStop producers briefly, let consumers finish the old layout, then resume with the new count. Costs minutes of ingest delay, buffered at the gateway.
- Order-tolerant consumersMost generalStamp each event with a per-trip sequence number. The consumer holds out-of-order events until the gap fills, or applies them by sequence on write.
Skew caps one partition at any count
Adding partitions spreads keys across more consumers, but every event for one key still lands in 1 partition. Suppose the events were keyed by city_id instead of trip_id, and the largest city produces 8% of events. At doubled peak that city sends 6,400 events per second into 1 partition, whose consumer tops out at 2,500. That partition's lag grows by 3,900 events per second whether the queue has 24 partitions or 480.
- Spreads load evenly: no single trip is hot.
- Keeps order per trip, which is all earnings needs.
- Loses per-city order, which nothing here uses.
- Key by city_id plus trip_id mod 4 for the largest city.
- Spreads 6,400 events per second as 1,600 into each of 4 partitions.
- Any per-city aggregate must now merge 4 partial results.
Scale on lag, not CPU
| Signal | Rises when | Where it misleads |
|---|---|---|
| CPU utilization | Work is compute-bound | Blind to consumers waiting on lookups |
| Lag in events | Input exceeds consumption | 1M events is 12.5 s of work at 80,000/s and about 4 minutes at 4,000/s |
| Lag age in seconds | Freshness is at risk | Reacts late to a sudden spike, since age grows slowly at first |
| Lag growth rate | Input exceeds capacity right now | Noisy; needs a smoothing window of a minute or more |
The reaction gap
Set the maximum from downstream quotas
What each plan costs per day
| Plan | Instance-hours per day | Cost per day | Risk |
|---|---|---|---|
| Static at 48 consumers | 1,152 | $230.40 | Pays for idle capacity 18 hours a day |
| Autoscaled, 16 off-peak to 48 for a 6-hour peak | 576 | $115.20 | Reaction gap on unplanned spikes |
| Static at 24 consumers | 576 | $115.20 | Fails the doubled peak outright |
| Static at 12 consumers with a profile cache | 288 | $57.60 | Enrichment reads tier data up to 10 minutes stale |
Costs assume $0.20 per instance-hour, a round figure for comparison; the cache's own cost is left out.
- Scale on lag age and lag growth together
- Cap max consumers at the narrowest downstream quota
- Pre-scale before peaks you can predict
- Make scale-in slower than scale-out
- Scale an IO-bound consumer on CPU
- Set max consumers above the partition count
- Buy instances before cutting per-event work
- Cache data that sets money without a correction path
Little's Law at each hop
| Hop | Throughput | Time inside | In flight |
|---|---|---|---|
| One consumer's lookups | 2,500/s | 4 ms | 10 concurrent calls, its 10 threads |
| Fleet lookups at doubled peak | 80,000/s | 4 ms | 320 concurrent calls at the profile service |
| One writer's buffer, today's peak | 6,667/s | 60 s between flushes | 400,000 events, 400 MB |
| One writer's buffer, doubled peak | 13,333/s | 60 s between flushes | 800,000 events, 800 MB |
Recovery is a separate calculation
| Configuration | Narrowest stage | Surplus over input | Time to drain |
|---|---|---|---|
| 24 consumers | Consumers, 60,000/s | None | Never; lag keeps growing |
| 48 consumers, 6 writers | Profile service and writers, 90,000/s | 10,000/s | 4 hours |
| 48 consumers, cache, 6 writers | Writers, 90,000/s | 10,000/s | 4 hours |
| 48 consumers, cache, 9 writers | Writers, 135,000/s | 55,000/s | About 44 minutes |
A degraded dependency
Measuring it in production
The same arithmetic runs against real metrics. The query below reads per-partition samples from a consumer_lag_metrics table, takes the latest sample for each partition, and estimates minutes to drain from the current surplus.
A NULL drain time marks a partition that is falling behind. The shape of the top 10 rows is the diagnosis: lag spread evenly across partitions means the fleet is short of capacity, while 1 or 2 partitions far above the rest point to a hot key that more consumers will not fix.
The redesigned pipeline: 96 partitions leave room for the next doubling, the cache cuts profile calls to 20% of events, and 9 writers, still the narrowest stage, raise the recovery ceiling to 135,000/s.
- DemandPeak of 80,000/sDoubled peak, 60% pings and 40% lifecycle events, lasting about 3 hours.
- First breakConsumers at 133%Lag age grows a quarter second per second and breaks the 5-minute target in 20 minutes.
- Steady-state fixCache, then partitionsA profile cache cuts per-event cost; 96 partitions leave room to scale consumers without repartitioning.
- Next ceilingWriters at 90,000/sWriter memory reaches 800 MB per writer, and 6 writers cap any drain at 10,000 events/s of surplus.
- RecoveryAbout 44 minutesThe backlog from 30 minutes down drains at 55,000 events/s of surplus, limited by the 9 writers.
- Overflow policyShed pings, buffer the restSample pings at the gateway when lag age passes 3 minutes; never drop lifecycle events.
- CostAutoscale with boundsBetween 12 and 48 consumers on lag signals, capped by the writer and profile quotas.
> A food delivery platform launches free delivery in a new country on a Friday evening, and order events double at dinner peak. Its order-events pipeline keys events by restaurant, enriches each one with a call to a menu-pricing service, and lands them in the table that feeds restaurant payouts every 5 minutes. Thirty minutes into the peak, payout freshness alerts fire, the pricing team reports their service is being throttled, and the most popular restaurant in the capital shows 40 minutes of lag while every other partition is current.
When load doubles, find the stage that breaks, the ceiling behind it, and how long recovery takes.
- Category
- Pipeline Architecture
- Difficulty
- advanced
- Duration
- 25 minutes
- Challenges
- 3 hands-on challenges
Topics covered: The Volume-Doubling Question, Backpressure Mechanisms, Horizontal Scaling With Partitions, Autoscaling and Cost Tradeoffs, End-to-End Throughput Analysis
Lesson Sections
- The Volume-Doubling Question
One question, many disguises Interviewers rarely say the word backpressure. They describe a change in load or in a dependency and wait to see whether you reason about rates or reach for a product feature. The forms below all reduce to the same model: work arrives at some rate, each stage processes it at some rate, and the gap between the two, held for some duration, decides what fails. Each version hands you 3 variables: the arrival rate, the capacity of the slowest stage, and how long the misma
- Backpressure Mechanisms (concepts: Stream Processing)
Backpressure is a stage telling the stage before it how much it can accept. With it, excess load waits in a place with a known bound or is refused at the edge. Without it, excess load piles up wherever nothing is counting, usually a process's heap, until the process dies and takes its in-flight work with it. Where excess load can go A pull-based queue provides buffering without extra design. Consumers fetch when they have room, so the backlog sits in the queue's partitions on disk, the lag is th
- Horizontal Scaling With Partitions (concepts: Partitioning Strategies)
A partition is the unit of both parallelism and ordering. Within a consumer group, each partition is assigned to 1 consumer at a time, which is what keeps events for a key in order. The same rule caps the group: with 24 partitions, the 25th consumer receives no assignment and idles. Partition count is the ceiling on consumer parallelism, so it has to be sized before load arrives. Sizing the partition count Divide the peak rate by what one consumer can sustain while running at a target fraction o
- Autoscaling and Cost Tradeoffs (concepts: Pipeline Monitoring)
Autoscaling makes consumer count a function of a measured signal. The design comes down to 3 choices: which signal, which bounds, and how fast to react. Each choice has a failure that shows up only under the load it was meant for. Scale on lag, not CPU An enrichment consumer spends most of each event waiting on the profile call. At full throughput its 10 threads are blocked on the network most of the time, and CPU sits around 30%. A rule that scales out at 70% CPU never fires, while lag climbs a
- End-to-End Throughput Analysis
A pipeline's throughput is the minimum of its stages' capacities, and the rest of the analysis follows from finding that minimum under each condition the interviewer names: steady peak, a spike, a degraded dependency, and recovery. The same stage is rarely the minimum in all 4. Little's Law at each hop Little's Law ties 3 quantities together for any stable stage: items in flight equal throughput times the time each item spends inside. It sizes concurrency, and it exposes memory limits that a thr