~learn.
?

Day 0433 minPreface+60 XP

Capacity Planning.

System Design Mastery Series — Production Readiness and Operational Excellence

4 stages4 code

watch it first · newThe whole lesson in a 17-minute animated lecture5 quiz checkpoints · ask the AI about any moment · captions, chapters and speed

Preface · 1 min

Preface#

You can define health, see health, and maintain health through change.

But what happens when Black Friday hits and traffic goes 10x?

The capacity nightmare

Friday, 11:47 PM. Your phone explodes.

  • ALERT: API latency p99 = 15 seconds (SLO: 200ms)
  • ALERT: Database connections exhausted
  • ALERT: Error rate 45% (SLO: 0.1%)
  • ALERT: Memory usage 98%
  • ALERT: Customer complaints spiking

You open your laptop. The dashboard is all red.

What happened?
  • Marketing launched a viral campaign
  • Traffic went 5x normal
  • Nobody told engineering
  • System wasn't ready
  • You're now firefighting at midnight

This didn't have to happen.

With capacity planning
  • You know your system's limits
  • You forecast future demand
  • You scale BEFORE you need to
  • Marketing campaign? Pre-scaled 24 hours ago
  • You're sleeping peacefully at midnight

This is capacity planning.

Today, we learn to answer: "How much traffic can my system handle, and when will I need more?"

ExplainerThe whole lesson: capacity planning

At 11:47 PM on a Friday a 5x viral campaign exhausts the database and pushes p99 latency to 15 seconds, while a planned system was pre-scaled a day earlier: capacity planning is a cycle of measure, forecast, identify, plan and validate. Headroom is spare capacity over maximum (a 3,000 QPS database at 2,000 has 33%; keep 30%), and load tests show latency within the SLO up to 1,000 RPS and breaking at 1,800. Growing 10% a month, 25% headroom in January becomes 17% in February, so scaling starts then; Little's Law sizes 500 RPS at 0.2 s as 100 in flight, about 36 pods with headroom, and queueing wait grows from 1x at 50% utilization to 9x at 90%. Smoke, load, stress, spike and soak tests, vertical or horizontal scaling and auto-scaling that takes 3 to 5 minutes complete the toolkit.

Part I · Stage 1 of 4 · 14 min

Part I: Foundations#

Chapter 1: What Is Capacity Planning?#

1.1 The Capacity Planning Process#

DiagramCapacity planning cycle
%% title: Capacity planning cycle
flowchart TD
  M["MEASURE Current<br/>What can we handle NOW?<br/>Current load, headroom, limits"]:::core
  F["FORECAST Demand<br/>What will we NEED?<br/>Growth trends, events, seasonality"]:::service
  I["IDENTIFY Bottlenecks<br/>What breaks FIRST?<br/>Database, memory, CPU, network"]:::bad
  P["PLAN Scaling<br/>What do we DO about it?<br/>Scale up, scale out, optimize"]:::service
  V["VALIDATE (Test)<br/>Does it WORK?<br/>Load testing, chaos engineering"]:::ok
  M --> F --> I --> P --> V
  V -->|"Back to MEASURE"| M

1.2 Key Metrics for Capacity#

Capacity metrics by layer
Application layer
  • Requests per second (RPS)
  • Active connections
  • Request queue depth
  • Response time (p50, p99)
  • Error rate

Question: How many requests can we handle before latency degrades?

Compute layer
  • CPU utilization (per core)
  • Memory usage and available
  • Thread/goroutine count
  • Garbage collection frequency
  • Context switches

Question: Are we CPU-bound or memory-bound?

Database layer
  • Queries per second
  • Connection pool usage
  • Query latency (p50, p99)
  • Replication lag
  • Disk I/O (reads/writes)
  • Buffer cache hit rate

Question: Is the database the bottleneck?

Cache layer
  • Hit rate
  • Miss rate
  • Memory usage
  • Eviction rate
  • Operations per second

Question: Is caching effective? Are we evicting too much?

Network layer
  • Bandwidth utilization
  • Packet loss
  • Connection errors
  • DNS lookup time

Question: Is the network a bottleneck?

1.3 The Concept of Headroom#

Headroom: the safety buffer

Headroom = (Maximum Capacity - Current Usage) / Maximum Capacity × 100%

Example
  • Database can handle: 3,000 QPS
  • Current usage: 2,000 QPS
  • Headroom: (3000 - 2000) / 3000 = 33%
Why headroom matters
  • Traffic is bursty (spikes above average)
  • Dependent services might fail (retry storms)
  • GC pauses, network blips happen
  • Need room to absorb unexpected load
  • Operating at 100% = already failing
Recommended headroom
Headroom % Status Action
> 50% Healthy Monitor, plan for growth
30-50% Adequate Start planning scaling
20-30% Warning Actively prepare scaling
10-20% Critical Scale NOW
< 10% Emergency You're probably already degraded

The rule of thumb: Maintain at least 30% headroom on critical resources.

Chapter 2: Measuring Current Capacity#

2.1 Establishing Baselines#

pythonResourceMetrics · 233 lines
# capacity/baseline.py

"""
Capacity baseline measurement and tracking.

Establishes what "normal" looks like for your system.
"""

from dataclasses import dataclass, field
from typing import Dict, List, Optional
from datetime import datetime, timedelta, timezone
from enum import Enum
import statistics


@dataclass
class ResourceMetrics:
    """Metrics for a single resource."""
    name: str
    current_value: float
    max_capacity: float
    unit: str
    
    @property
    def utilization(self) -> float:
        """Current utilization percentage."""
        return (self.current_value / self.max_capacity) * 100 if self.max_capacity > 0 else 0
    
    @property
    def headroom(self) -> float:
        """Available headroom percentage."""
        return 100 - self.utilization
    
    @property
    def status(self) -> str:
        """Health status based on headroom."""
        if self.headroom > 50:
            return "healthy"
        elif self.headroom > 30:
            return "adequate"
        elif self.headroom > 20:
            return "warning"
        elif self.headroom > 10:
            return "critical"
        else:
            return "emergency"


@dataclass
class CapacityBaseline:
    """Baseline capacity measurements for a service."""
    service_name: str
    measured_at: datetime
    
    # Traffic metrics
    avg_rps: float
    peak_rps: float
    p50_latency_ms: float
    p99_latency_ms: float
    
    # Resource metrics
    resources: Dict[str, ResourceMetrics] = field(default_factory=dict)
    
    # Derived limits
    max_sustainable_rps: float = 0  # RPS before latency degrades
    breaking_point_rps: float = 0   # RPS when errors start


class CapacityMeasurer:
    """
    Measures and tracks system capacity.
    """
    
    def __init__(self, metrics_client, config):
        self.metrics = metrics_client
        self.config = config
    
    async def measure_baseline(
        self,
        service_name: str,
        window: str = "7d"
    ) -> CapacityBaseline:
        """
        Measure baseline capacity from historical data.
        """
        # Traffic metrics
        avg_rps = await self.metrics.query(
            f'avg(rate(http_requests_total{{service="{service_name}"}}[5m]))',
            window=window
        )
        
        peak_rps = await self.metrics.query(
            f'max(rate(http_requests_total{{service="{service_name}"}}[5m]))',
            window=window
        )
        
        p50_latency = await self.metrics.query(
            f'histogram_quantile(0.5, sum by (le) (rate(http_request_duration_seconds_bucket{{service="{service_name}"}}[5m])))',
            window=window
        ) * 1000  # Convert to ms
        
        p99_latency = await self.metrics.query(
            f'histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{{service="{service_name}"}}[5m])))',
            window=window
        ) * 1000
        
        # Resource metrics
        resources = {}
        
        # CPU
        cpu_usage = await self.metrics.query(
            f'avg(rate(container_cpu_usage_seconds_total{{service="{service_name}"}}[5m]))',
            window=window
        )
        cpu_limit = await self.metrics.query(
            f'avg(container_spec_cpu_quota{{service="{service_name}"}} / container_spec_cpu_period{{service="{service_name}"}})',
            window=window
        )
        resources['cpu'] = ResourceMetrics(
            name='CPU',
            current_value=cpu_usage,
            max_capacity=cpu_limit,
            unit='cores'
        )
        
        # Memory
        memory_usage = await self.metrics.query(
            f'avg(container_memory_working_set_bytes{{service="{service_name}"}})',
            window=window
        ) / (1024 ** 3)  # Convert to GB
        memory_limit = await self.metrics.query(
            f'avg(container_spec_memory_limit_bytes{{service="{service_name}"}})',
            window=window
        ) / (1024 ** 3)
        resources['memory'] = ResourceMetrics(
            name='Memory',
            current_value=memory_usage,
            max_capacity=memory_limit,
            unit='GB'
        )
        
        # Database connections
        db_connections = await self.metrics.query(
            f'avg(db_connections_active{{service="{service_name}"}})',
            window=window
        )
        db_max_connections = await self.metrics.query(
            f'avg(db_connections_max{{service="{service_name}"}})',
            window=window
        )
        resources['db_connections'] = ResourceMetrics(
            name='Database Connections',
            current_value=db_connections,
            max_capacity=db_max_connections,
            unit='connections'
        )
        
        return CapacityBaseline(
            service_name=service_name,
            measured_at=datetime.now(timezone.utc),
            avg_rps=avg_rps,
            peak_rps=peak_rps,
            p50_latency_ms=p50_latency,
            p99_latency_ms=p99_latency,
            resources=resources
        )
    
    async def generate_capacity_report(
        self,
        service_name: str
    ) -> str:
        """
        Generate a human-readable capacity report.
        """
        baseline = await self.measure_baseline(service_name)
        
        report = f"""
CAPACITY REPORT: {service_name}
Generated: {baseline.measured_at.isoformat()}
{'=' * 60}

TRAFFIC METRICS
---------------
Average RPS:      {baseline.avg_rps:.1f}
Peak RPS:         {baseline.peak_rps:.1f}
P50 Latency:      {baseline.p50_latency_ms:.1f}ms
P99 Latency:      {baseline.p99_latency_ms:.1f}ms

RESOURCE UTILIZATION
--------------------
"""
        for name, resource in baseline.resources.items():
            status_emoji = {
                'healthy': '🟢',
                'adequate': '🟡',
                'warning': '🟠',
                'critical': '🔴',
                'emergency': '⚫'
            }.get(resource.status, '⚪')
            
            report += f"""
{resource.name}:
  Current:   {resource.current_value:.2f} {resource.unit}
  Maximum:   {resource.max_capacity:.2f} {resource.unit}
  Usage:     {resource.utilization:.1f}%
  Headroom:  {resource.headroom:.1f}%
  Status:    {status_emoji} {resource.status.upper()}
"""
        
        # Identify bottleneck
        bottleneck = min(
            baseline.resources.values(),
            key=lambda r: r.headroom
        )
        
        report += f"""
{'=' * 60}
BOTTLENECK ANALYSIS
-------------------
Primary bottleneck: {bottleneck.name}
  - Only {bottleneck.headroom:.1f}% headroom remaining
  - Will likely be first to saturate under load

RECOMMENDATIONS
---------------
"""
        for name, resource in baseline.resources.items():
            if resource.headroom < 30:
                report += f"  ⚠️  {resource.name}: Prepare or start scaling (headroom < 30%)\n"
            elif resource.headroom < 50:
                report += f"  📋 {resource.name}: Start planning scaling\n"
        
        return report

2.2 Understanding Your Limits#

Finding your breaking point

You need to know:

  1. At what load does latency start degrading?
  2. At what load do errors start appearing?
  3. At what load does the system fall over completely?
Latency vs load curve

Latency (ms, axis 100 → 5000) rises with load (RPS, axis 500 → 2000): flat near 100–200 ms at low load, crossing the 200 ms SLO Target, then Degradation Begins around 500 ms, and the curve shoots past 1000–5000 ms at the Breaking Point.

  • 1000 RPS — Sustainable max (latency still within SLO)
  • 1800 RPS — Breaking point (errors start)
Key points
Load What happens
Up to 1000 RPS Latency stable, within SLO
1000-1500 RPS Latency rising past the SLO target
1500-1800 RPS Severe degradation, users clearly affected
1800+ RPS System failing, errors appearing

SUSTAINABLE CAPACITY = 1000 RPS. Plan to run at no more than ~700 RPS (30% headroom below it).

Quick check3 questions · +5 XP each

Headroom and current limits

  1. Q1/3 Your database maxes out at 3,000 QPS and peaks at 2,250 QPS. What is its headroom, and what should you do?

    Answer & explanation

    Answer: C

    Headroom = (3,000 - 2,250) / 3,000 = 25%, which falls in the 20-30% warning band. 75% is the utilization, not the headroom, and 33% comes from dividing by current usage instead of maximum capacity.

  2. Q2/3 Latency looks fine with a service at 95% CPU. Why is that still a problem?

    Answer & explanation

    Answer: A

    Traffic is bursty and failures cause retries; with almost no headroom, the next spike pushes you to 100%, which means already failing. The lesson recommends keeping at least 30% headroom on critical resources for exactly this reason.

  3. Q3/3 A stress test maps your latency-vs-load curve. Which load should you plan around as your capacity?

    Answer & explanation

    Answer: D

    Capacity is the sustainable maximum, the point before latency degrades past the SLO, with a buffer kept on top. By the time errors appear, users are already being failed, so that point is the breaking point, not capacity.

Chapter 3: Forecasting Demand#

3.1 Growth Modeling#

pythonTrafficForecast · 193 lines
# capacity/forecasting.py

"""
Demand forecasting for capacity planning.

Predicts future load based on historical trends and known events.
"""

from dataclasses import dataclass
from typing import List, Optional, Tuple
from datetime import datetime, timedelta, timezone
import math


@dataclass
class TrafficForecast:
    """Forecast for future traffic."""
    date: datetime
    predicted_rps: float
    confidence_low: float
    confidence_high: float
    factors: List[str]  # What's driving this prediction


@dataclass
class GrowthModel:
    """Model for traffic growth."""
    base_rps: float
    monthly_growth_rate: float  # e.g., 0.10 for 10% MoM
    seasonality: dict  # Month -> multiplier
    day_of_week_pattern: dict  # Day -> multiplier
    hour_of_day_pattern: dict  # Hour -> multiplier


class DemandForecaster:
    """
    Forecasts future demand based on historical patterns.
    """
    
    def __init__(self, metrics_client):
        self.metrics = metrics_client
    
    async def build_growth_model(
        self,
        service_name: str,
        lookback_months: int = 6
    ) -> GrowthModel:
        """
        Build growth model from historical data.
        """
        # Get monthly averages
        monthly_data = []
        for i in range(lookback_months, 0, -1):
            start = datetime.now(timezone.utc) - timedelta(days=30 * i)
            end = start + timedelta(days=30)
            
            avg_rps = await self.metrics.query_range(
                f'avg(rate(http_requests_total{{service="{service_name}"}}[1h]))',
                start=start,
                end=end
            )
            monthly_data.append(avg_rps.mean())
        
        # Calculate growth rate
        if len(monthly_data) >= 2:
            # Compound monthly growth rate
            start_value = monthly_data[0]
            end_value = monthly_data[-1]
            months = len(monthly_data) - 1
            
            if start_value > 0:
                growth_rate = (end_value / start_value) ** (1 / months) - 1
            else:
                growth_rate = 0
        else:
            growth_rate = 0
        
        # Get seasonality (month of year patterns)
        seasonality = await self._calculate_seasonality(service_name)
        
        # Get day-of-week patterns
        dow_pattern = await self._calculate_dow_pattern(service_name)
        
        # Get hour-of-day patterns
        hod_pattern = await self._calculate_hod_pattern(service_name)
        
        return GrowthModel(
            base_rps=monthly_data[-1] if monthly_data else 0,
            monthly_growth_rate=growth_rate,
            seasonality=seasonality,
            day_of_week_pattern=dow_pattern,
            hour_of_day_pattern=hod_pattern
        )
    
    def forecast(
        self,
        model: GrowthModel,
        target_date: datetime,
        include_events: List[dict] = None
    ) -> TrafficForecast:
        """
        Forecast traffic for a target date.
        """
        now = datetime.now(timezone.utc)
        months_ahead = (target_date - now).days / 30
        
        # Base growth projection
        base_projection = model.base_rps * ((1 + model.monthly_growth_rate) ** months_ahead)
        
        # Apply seasonality
        month = target_date.month
        seasonality_factor = model.seasonality.get(month, 1.0)
        
        # Apply day-of-week pattern
        dow = target_date.weekday()
        dow_factor = model.day_of_week_pattern.get(dow, 1.0)
        
        # Apply hour pattern for peak hour
        peak_hour_factor = max(model.hour_of_day_pattern.values())
        
        # Calculate predicted RPS
        predicted_rps = base_projection * seasonality_factor * dow_factor * peak_hour_factor
        
        # Apply event multipliers
        factors = ["organic_growth", f"seasonality_{month}", f"dow_{dow}"]
        
        if include_events:
            for event in include_events:
                if event['date'].date() == target_date.date():
                    predicted_rps *= event['traffic_multiplier']
                    factors.append(event['name'])
        
        # Confidence intervals (simplified)
        confidence_low = predicted_rps * 0.8
        confidence_high = predicted_rps * 1.4
        
        return TrafficForecast(
            date=target_date,
            predicted_rps=predicted_rps,
            confidence_low=confidence_low,
            confidence_high=confidence_high,
            factors=factors
        )
    
    def forecast_range(
        self,
        model: GrowthModel,
        start_date: datetime,
        end_date: datetime,
        known_events: List[dict] = None
    ) -> List[TrafficForecast]:
        """
        Forecast traffic for a date range.
        """
        forecasts = []
        current = start_date
        
        while current <= end_date:
            forecast = self.forecast(model, current, known_events)
            forecasts.append(forecast)
            current += timedelta(days=1)
        
        return forecasts


# =============================================================================
# EXAMPLE: PLANNING FOR KNOWN EVENTS
# =============================================================================

"""
Known events that affect traffic:

BLACK_FRIDAY = {
    'name': 'Black Friday',
    'date': datetime(2024, 11, 29),
    'traffic_multiplier': 5.0,  # 5x normal traffic
    'duration_hours': 24
}

PRODUCT_LAUNCH = {
    'name': 'Product Launch',
    'date': datetime(2024, 10, 15),
    'traffic_multiplier': 3.0,
    'duration_hours': 48
}

MARKETING_CAMPAIGN = {
    'name': 'TV Ad Campaign',
    'date': datetime(2024, 9, 1),
    'traffic_multiplier': 2.0,
    'duration_hours': 168  # 1 week
}
"""
Your turn · checkpoint 1not answered

What breaks if you wait for the alarm?

In January your peak is 1,500 RPS against a sustainable 2,000 RPS, and traffic grows 10% a month. A scaling project takes 4–6 weeks. Your rule is "start scaling when headroom drops below 10%". In which month does that rule fire, and what happens to users while the project runs?

Think it through, or jot it down. Then compare.

One strong answer

  • Peaks: February 1,650 (17% headroom), March 1,815 (9%), April ~2,000 (none left). The rule fires in March.
  • Then the project needs 4–6 weeks, so April's traffic arrives before the new capacity does: latency past the SLO and errors at peak.
  • Start in February, when headroom first drops below 20% and there's still room to finish before you run out.

3.2 Capacity Timeline#

Capacity timeline planning
Current: January
  • Traffic: 1,000 RPS average, 1,500 peak
  • Capacity: 2,000 RPS (sustainable)
  • Headroom at peak: (2,000 − 1,500) / 2,000 = 25%
Forecast
Month Projected Peak Capacity Headroom Action Needed
January 1,500 RPS 2,000 25% Warning: prepare scaling
February 1,650 RPS 2,000 17% Critical: SCALE NOW
March 1,815 RPS 2,000 9% Emergency if not scaled yet
April 2,000 RPS 2,500 20% After scaling
May 2,200 RPS 2,500 12% Critical: scale again
...
November ~3,900 RPS ??? ??? Black Friday!

(Black Friday) × 5 ≈ 19,500 RPS! (1,500 × 1.1¹⁰ ≈ 3,890 RPS normal November peak)

Action timeline
  • January: Establish baseline, prepare the scaling project (25% is already the warning band)
  • February: Begin scaling project (lead time: 4-6 weeks)
  • March: Complete scaling to 2,500 RPS
  • Q2: Plan for 4,000 RPS capacity (Q3 target)
  • Q3: Begin Black Friday planning
  • October: Pre-scale for Black Friday
  • November: Execute Black Friday capacity plan

Chapter 4: Identifying Bottlenecks#

4.1 Bottleneck Analysis#

Finding the bottleneck

The bottleneck is the component that limits system throughput. It's the FIRST thing to saturate under load.

Common bottlenecks:

1. Database

Symptoms:

  • Slow query times
  • Connection pool exhaustion
  • Lock contention
  • High CPU on database server

Fixes:

  • Add read replicas
  • Add caching layer
  • Optimize queries
  • Connection pooling (PgBouncer)
  • Shard the database
2. Application CPU

Symptoms:

  • High CPU utilization across pods
  • Latency increases with load
  • Adding pods helps

Fixes:

  • Scale horizontally (add pods)
  • Optimize hot code paths
  • Use faster serialization
  • Enable async processing
3. Memory

Symptoms:

  • OOM kills
  • Excessive GC pauses
  • Swap usage

Fixes:

  • Increase memory limits
  • Fix memory leaks
  • Tune GC settings
  • Stream instead of buffering
4. External dependencies

Symptoms:

  • Timeouts to specific service
  • Queue buildup for async calls
  • Rate limiting from external APIs

Fixes:

  • Add caching
  • Implement circuit breakers
  • Negotiate higher rate limits
  • Add fallback providers
5. Network

Symptoms:

  • High bandwidth utilization
  • Connection timeouts
  • Packet loss

Fixes:

  • Compress responses
  • Use CDN for static content
  • Upgrade network capacity
  • Optimize payload sizes
Your turn · checkpoint 2not answered

Estimate: how many requests are in flight?

Your API receives 100 requests a second and each takes 200 ms on average. How many requests are being handled at any moment? You run 10 pods with 2 workers each. What happens if traffic rises to 150 requests a second?

Think it through, or jot it down. Then compare.

One strong answer

  • In flight: 100 × 0.2 s = 20 requests, exactly your 20 workers. You're at capacity with no headroom.
  • At 150 a second you'd need 30 workers. The extra requests queue, waiting makes each one take longer, which puts even more in flight.
  • So latency doesn't rise gently. It spirals, and you need more workers before traffic rises, not after.

4.2 Little's Law#

WorksheetLittle's Law: the fundamental equation

L = λ × W

Where:

  • L = Average number of items in system (queue + being processed)
  • λ = Arrival rate (requests per second)
  • W = Average time in system (seconds)
Example — your API
  • Receives 100 requests/second (λ = 100)
  • Average response time is 200ms (W = 0.2s)
  • Therefore: L = 100 × 0.2 = 20 concurrent requests

If you have 10 pods with 2 workers each = 20 workers total. You're AT CAPACITY!

What happens if traffic increases to 150 RPS?
  • L = 150 × 0.2 = 30 concurrent requests needed
  • You only have 20 workers
  • Requests queue up
  • Queue adds to W (wait time)
  • W increases → L increases → more queuing
  • SPIRAL INTO FAILURE
Using Little's Law for capacity

To handle 500 RPS with 200ms response time:

  • L = 500 × 0.2 = 100 concurrent requests
  • Need: 100 workers (or threads/connections)
  • With 4 workers per pod: 100/4 = 25 pods
  • Keep 30% headroom (run at 70% of capacity): 25 / 0.7 ≈ 36 pods

This is your capacity plan!

4.3 Queueing Theory Basics#

Queueing theory for engineers

When utilization approaches 100%, latency goes to infinity.

Wait time vs utilization (0% → 100%): the curve stays low at first, then bends sharply upward and shoots to infinity as utilization nears 100% — the Danger Zone.

Key insight (single-server M/M/1 queue: wait ≈ service time × ρ / (1 − ρ))
  • At 50% utilization: Wait ≈ 1× service time
  • At 80% utilization: Wait ≈ 4× service time (4x the 50% level)
  • At 90% utilization: Wait ≈ 9× service time (9x the 50% level)
  • At 100%: Infinite queue, system fails

RULE: Never operate above 70-80% utilization on critical resources

ExplainerLittle's Law and the queueing cliff

L = λ × W: 100 requests/s at 0.2 s keeps 20 requests in flight, exactly 10 pods × 2 workers. At 150/s 30 are needed, so requests queue, W grows and L with it. In a single-server queue, wait ≈ service time × ρ/(1−ρ): 1× at 50% utilization, 4× at 80%, 9× at 90%, unbounded at 100%.

ExplainerBreak it: utilisation vs queueing delay

Sliders for utilisation and service time on the lesson's single-server M/M/1 model, wait = service time × ρ ÷ (1 − ρ): about 1× the service time at 50%, 4× at 80%, 9× at 90% and unbounded at 100%. It also shows the wait with 10% more traffic (from 90% busy that is 99%, and 9× becomes 99×) and the queue length ρ² ÷ (1 − ρ). A model, not a measurement: keep critical resources under 70–80%.

Quick check3 questions · +5 XP each

Forecasting and bottlenecks

  1. Q1/3 Using Little's Law: an API gets 400 requests/second with a 250ms average response time, and each pod has 4 workers. How many pods do you need at minimum, before adding headroom?

    Answer & explanation

    Answer: B

    L = 400 x 0.25 = 100 concurrent requests, and 100 / 4 workers per pod = 25 pods; then add headroom on top. 100 is the number of workers needed, not the number of pods.

  2. Q2/3 You have 20 workers serving 100 RPS at 200ms, which is exactly at capacity. Traffic rises to 150 RPS. What happens?

    Answer & explanation

    Answer: D

    150 x 0.2 = 30 concurrent requests are needed against 20 workers, so the queue grows and raises W, which raises L again. Queueing is not a linear slowdown; near full utilization, wait time climbs toward infinity.

  3. Q3/3 A forecast shows headroom falling to 17% in February and 9% in March. Why start the scaling project in February instead of waiting for March?

    Answer & explanation

    Answer: A

    Scaling projects take weeks, so you start while there is still room: 17% is already the critical, scale-now band, and 9% is the emergency band where you are probably degraded. Waiting for the problem to show up is the reactive pattern that capacity planning exists to avoid.

Part II · Stage 2 of 4 · 10 min

Part II: Implementation#

Your turn · checkpoint 3not answered

Design it: which test answers which question?

You have three open questions about the checkout service: at what load does it start failing; does it survive a sudden jump from 2× to 10× normal traffic and back; and does memory creep up over a day at normal load? Which kind of load test answers each, and roughly how long does each run?

Think it through, or jot it down. Then compare.

One strong answer

  • Where it breaks: a stress test, raising load step by step until errors appear.
  • The sudden jump: a spike test, a few minutes at 10× between periods at 2×, to check auto-scaling and graceful degradation.
  • Memory creep: a soak test at normal load for hours or days. A short test never runs long enough to show a slow leak.

Chapter 5: Load Testing#

5.1 Load Testing Strategy#

Load testing types
Test Purpose Load Duration Goal
1. Smoke test Minimal load to verify system works. Very low (1-10 users) Minutes Verify test setup, basic functionality
2. Load test Expected load to verify normal operation. Normal production traffic 30-60 minutes Verify system handles normal load
3. Stress test Beyond normal load to find breaking point. Gradually increase until failure Until break or max reached Find system limits
4. Spike test Sudden traffic spike to test elasticity. Sudden jump (e.g., 2x → 10x → 2x) Short spikes (minutes) Verify auto-scaling, graceful degradation
5. Soak test Extended duration to find memory leaks, drift. Normal to moderate Hours to days Find issues that emerge over time

5.2 Load Test Implementation#

pythonLoadPattern · 268 lines
# capacity/load_testing.py

"""
Load testing orchestration and analysis.

Uses k6, Locust, or similar tools under the hood.
"""

from dataclasses import dataclass, field
from typing import List, Dict, Optional
from datetime import datetime, timedelta, timezone
from enum import Enum
import asyncio
import subprocess
import json


class LoadPattern(Enum):
    CONSTANT = "constant"
    RAMP_UP = "ramp_up"
    SPIKE = "spike"
    STRESS = "stress"


@dataclass
class LoadTestConfig:
    """Configuration for a load test."""
    name: str
    target_url: str
    
    # Load profile
    pattern: LoadPattern
    virtual_users: int
    ramp_up_time_seconds: int = 60
    duration_seconds: int = 300
    
    # For spike tests
    spike_users: int = 0
    spike_duration_seconds: int = 60
    
    # Thresholds
    max_latency_p99_ms: int = 500
    max_error_rate: float = 0.01
    
    # Scenarios
    scenarios: List[dict] = field(default_factory=list)


@dataclass
class LoadTestResult:
    """Results from a load test."""
    config: LoadTestConfig
    started_at: datetime
    completed_at: datetime
    
    # Summary metrics
    total_requests: int
    successful_requests: int
    failed_requests: int
    
    # Latency
    latency_p50_ms: float
    latency_p95_ms: float
    latency_p99_ms: float
    latency_max_ms: float
    
    # Throughput
    requests_per_second: float
    
    # Thresholds
    passed: bool
    threshold_violations: List[str]
    
    # Raw data for analysis
    timeseries_data: List[dict] = field(default_factory=list)


class LoadTestRunner:
    """
    Runs and analyzes load tests.
    """
    
    def __init__(self, metrics_client, alerter):
        self.metrics = metrics_client
        self.alerter = alerter
    
    async def run_test(
        self,
        config: LoadTestConfig,
        notify_on_failure: bool = True
    ) -> LoadTestResult:
        """
        Run a load test and analyze results.
        """
        started_at = datetime.now(timezone.utc)
        
        # Generate k6 script
        script = self._generate_k6_script(config)
        
        # Run k6
        result_json = await self._execute_k6(script, config)
        
        # Parse results
        result = self._parse_results(config, result_json, started_at)
        
        # Check thresholds
        result.passed, result.threshold_violations = self._check_thresholds(
            config, result
        )
        
        # Notify if failed
        if not result.passed and notify_on_failure:
            await self.alerter.send_warning(
                f"Load test '{config.name}' failed: {result.threshold_violations}"
            )
        
        # Store results
        await self._store_results(result)
        
        return result
    
    def _generate_k6_script(self, config: LoadTestConfig) -> str:
        """Generate k6 test script."""
        
        if config.pattern == LoadPattern.CONSTANT:
            stages = f"""
                stages: [
                    {{ duration: '{config.ramp_up_time_seconds}s', target: {config.virtual_users} }},
                    {{ duration: '{config.duration_seconds}s', target: {config.virtual_users} }},
                    {{ duration: '30s', target: 0 }},
                ]
            """
        elif config.pattern == LoadPattern.RAMP_UP:
            stages = f"""
                stages: [
                    {{ duration: '{config.duration_seconds}s', target: {config.virtual_users} }},
                    {{ duration: '60s', target: 0 }},
                ]
            """
        elif config.pattern == LoadPattern.SPIKE:
            stages = f"""
                stages: [
                    {{ duration: '60s', target: {config.virtual_users} }},
                    {{ duration: '{config.spike_duration_seconds}s', target: {config.spike_users} }},
                    {{ duration: '60s', target: {config.virtual_users} }},
                    {{ duration: '30s', target: 0 }},
                ]
            """
        elif config.pattern == LoadPattern.STRESS:
            # Gradually increase until we find the breaking point
            stages = f"""
                stages: [
                    {{ duration: '60s', target: {config.virtual_users} }},
                    {{ duration: '120s', target: {config.virtual_users * 2} }},
                    {{ duration: '120s', target: {config.virtual_users * 4} }},
                    {{ duration: '120s', target: {config.virtual_users * 8} }},
                    {{ duration: '60s', target: 0 }},
                ]
            """
        
        script = f"""
import http from 'k6/http';
import {{ check, sleep }} from 'k6';
import {{ Rate, Trend }} from 'k6/metrics';

const errorRate = new Rate('errors');
const latency = new Trend('latency');

export let options = {{
    {stages},
    thresholds: {{
        'http_req_duration': ['p(99)<{config.max_latency_p99_ms}'],
        'errors': ['rate<{config.max_error_rate}'],
    }},
}};

export default function() {{
    const res = http.get('{config.target_url}');
    
    const success = check(res, {{
        'status is 200': (r) => r.status === 200,
        'latency < 500ms': (r) => r.timings.duration < 500,
    }});
    
    errorRate.add(!success);
    latency.add(res.timings.duration);
    
    sleep(1);
}}
"""
        return script
    
    async def _execute_k6(self, script: str, config: LoadTestConfig) -> dict:
        """Execute k6 and return JSON results."""
        # Write script to temp file
        script_path = f'/tmp/k6_script_{config.name}.js'
        with open(script_path, 'w') as f:
            f.write(script)
        
        # Run k6 and export the end-of-test summary as one JSON document
        # (--out json=... writes one JSON object per line, not a single document)
        result = subprocess.run(
            ['k6', 'run', '--summary-export', '/tmp/k6_summary.json', script_path],
            capture_output=True,
            text=True
        )
        
        # Parse JSON summary
        with open('/tmp/k6_summary.json', 'r') as f:
            return json.load(f)
    
    def _check_thresholds(
        self,
        config: LoadTestConfig,
        result: LoadTestResult
    ) -> tuple[bool, List[str]]:
        """Check if results meet thresholds."""
        violations = []
        
        if result.latency_p99_ms > config.max_latency_p99_ms:
            violations.append(
                f"P99 latency {result.latency_p99_ms}ms > threshold {config.max_latency_p99_ms}ms"
            )
        
        error_rate = result.failed_requests / result.total_requests
        if error_rate > config.max_error_rate:
            violations.append(
                f"Error rate {error_rate:.2%} > threshold {config.max_error_rate:.2%}"
            )
        
        return len(violations) == 0, violations


# =============================================================================
# EXAMPLE LOAD TEST SCENARIOS
# =============================================================================

API_LOAD_TEST = LoadTestConfig(
    name="api_baseline",
    target_url="https://api.example.com/health",
    pattern=LoadPattern.CONSTANT,
    virtual_users=100,
    duration_seconds=300,
    max_latency_p99_ms=200,
    max_error_rate=0.01
)

BLACK_FRIDAY_TEST = LoadTestConfig(
    name="black_friday_simulation",
    target_url="https://api.example.com/checkout",
    pattern=LoadPattern.SPIKE,
    virtual_users=500,
    spike_users=5000,
    spike_duration_seconds=120,
    duration_seconds=600,
    max_latency_p99_ms=1000,
    max_error_rate=0.05
)

STRESS_TEST = LoadTestConfig(
    name="find_breaking_point",
    target_url="https://api.example.com/api",
    pattern=LoadPattern.STRESS,
    virtual_users=100,
    duration_seconds=600,
    max_latency_p99_ms=5000,
    max_error_rate=0.10
)

Chapter 6: Scaling Strategies#

Your turn · checkpoint 4not answered

Trade-off: bigger machines or more machines?

Both your stateless API pods and your single PostgreSQL primary are running hot. For each, would you scale up (a bigger machine) or out (more machines)? What does each choice cost?

Think it through, or jot it down. Then compare.

One strong answer

  • API pods: scale out. They're stateless, so adding pods behind the load balancer is easy and removes single points of failure.
  • The primary database: scale up first. It's simple, needs no code changes and adds no distributed complexity, so it's a good quick fix.
  • The cost of scaling up: hardware limits, a single point of failure, a high price at the top end and usually some downtime. That's why read replicas or sharding come next for the long term.

6.1 Vertical vs Horizontal Scaling#

Scaling strategies

Vertical scaling (Scale Up)

Before → After: 4 CPU · 8 GB → 16 CPU · 64 GB

Pros:

  • Simple (no code changes)
  • No distributed complexity
  • Good for databases

Cons:

  • Limited by hardware
  • Single point of failure
  • Expensive at high end
  • Usually requires downtime

When to use — Vertical (Scale Up):

  • Databases (primary)
  • Single-threaded workloads
  • When simplicity matters most
  • As a quick fix while planning horizontal

Horizontal scaling (Scale Out)

Before → After: Pod → Pod 1 Pod 2 Pod 3 Pod 4

Pros:

  • Nearly unlimited scaling
  • No single point of failure
  • Can scale incrementally
  • Often cheaper at scale

Cons:

  • Distributed complexity
  • Stateless requirement
  • Load balancing needed
  • Data consistency challenges

When to use — Horizontal (Scale Out):

  • Stateless web services
  • Read replicas
  • Worker pools
  • For long-term scalability

6.2 Auto-Scaling Configuration#

pythonScalingMetric · 206 lines
# capacity/autoscaling.py

"""
Auto-scaling configuration and policies.

Defines when and how to scale automatically.
"""

from dataclasses import dataclass
from typing import List, Optional
from enum import Enum


class ScalingMetric(Enum):
    CPU = "cpu"
    MEMORY = "memory"
    RPS = "requests_per_second"
    QUEUE_DEPTH = "queue_depth"
    LATENCY = "latency_p99"
    CUSTOM = "custom"


@dataclass
class ScalingPolicy:
    """Defines when to scale."""
    name: str
    metric: ScalingMetric
    
    # Thresholds
    scale_up_threshold: float
    scale_down_threshold: float
    
    # Behavior
    scale_up_cooldown_seconds: int = 300
    scale_down_cooldown_seconds: int = 600
    
    # Limits
    min_replicas: int = 2
    max_replicas: int = 100
    
    # Aggressiveness
    scale_up_by: int = 2  # Add 2 pods at a time
    scale_down_by: int = 1  # Remove 1 pod at a time


@dataclass
class AutoScalingConfig:
    """Complete auto-scaling configuration."""
    deployment_name: str
    namespace: str
    
    # Policies (can have multiple)
    policies: List[ScalingPolicy]
    
    # Predictive scaling (for known events)
    scheduled_scaling: List[dict] = None
    
    # Protection
    disable_scale_down_during_deployment: bool = True
    

# =============================================================================
# EXAMPLE CONFIGURATIONS
# =============================================================================

# Web API: Scale on CPU and latency
WEB_API_SCALING = AutoScalingConfig(
    deployment_name="api-server",
    namespace="production",
    policies=[
        # Primary: CPU-based scaling
        ScalingPolicy(
            name="cpu-scaling",
            metric=ScalingMetric.CPU,
            scale_up_threshold=70,  # Scale up when CPU > 70%
            scale_down_threshold=30,  # Scale down when CPU < 30%
            min_replicas=3,
            max_replicas=50,
            scale_up_by=3,
            scale_down_by=1,
            scale_down_cooldown_seconds=900  # 15 min cooldown
        ),
        # Secondary: Latency-based scaling
        ScalingPolicy(
            name="latency-scaling",
            metric=ScalingMetric.LATENCY,
            scale_up_threshold=200,  # Scale up when p99 > 200ms
            scale_down_threshold=50,  # Scale down when p99 < 50ms
            min_replicas=3,
            max_replicas=50,
            scale_up_by=5,  # More aggressive for latency
            scale_up_cooldown_seconds=60  # React faster to latency
        ),
    ],
    scheduled_scaling=[
        # Pre-scale for known traffic patterns
        {
            "name": "morning_ramp",
            "schedule": "0 8 * * 1-5",  # 8 AM weekdays
            "min_replicas": 10
        },
        {
            "name": "evening_scale_down",
            "schedule": "0 22 * * *",  # 10 PM daily
            "min_replicas": 3
        },
        {
            "name": "black_friday",
            # Cron can't express "Thanksgiving week" ("0 0 * 11 5" would mean
            # every Friday in November), so pin the dates and update yearly
            "schedule": "0 0 25-29 11 *",  # Nov 25-29, 2024 (Thanksgiving week)
            "min_replicas": 50,
            "max_replicas": 200
        },
    ]
)

# Worker pool: Scale on queue depth
WORKER_SCALING = AutoScalingConfig(
    deployment_name="worker-pool",
    namespace="production",
    policies=[
        ScalingPolicy(
            name="queue-scaling",
            metric=ScalingMetric.QUEUE_DEPTH,
            scale_up_threshold=1000,  # Scale up when queue > 1000
            scale_down_threshold=100,  # Scale down when queue < 100
            min_replicas=2,
            max_replicas=20,
            scale_up_by=2,
            scale_up_cooldown_seconds=60  # React quickly to queue buildup
        ),
    ]
)


def generate_kubernetes_hpa(config: AutoScalingConfig) -> dict:
    """Generate Kubernetes HPA manifest from config."""
    
    hpa = {
        "apiVersion": "autoscaling/v2",
        "kind": "HorizontalPodAutoscaler",
        "metadata": {
            "name": f"{config.deployment_name}-hpa",
            "namespace": config.namespace
        },
        "spec": {
            "scaleTargetRef": {
                "apiVersion": "apps/v1",
                "kind": "Deployment",
                "name": config.deployment_name
            },
            "minReplicas": min(p.min_replicas for p in config.policies),
            "maxReplicas": max(p.max_replicas for p in config.policies),
            "metrics": [],
            "behavior": {
                "scaleUp": {
                    "stabilizationWindowSeconds": 60,
                    "policies": [
                        {
                            "type": "Pods",
                            "value": max(p.scale_up_by for p in config.policies),
                            "periodSeconds": 60
                        }
                    ]
                },
                "scaleDown": {
                    "stabilizationWindowSeconds": 300,
                    "policies": [
                        {
                            "type": "Pods",
                            "value": 1,
                            "periodSeconds": 120
                        }
                    ]
                }
            }
        }
    }
    
    # Add metrics
    for policy in config.policies:
        if policy.metric == ScalingMetric.CPU:
            hpa["spec"]["metrics"].append({
                "type": "Resource",
                "resource": {
                    "name": "cpu",
                    "target": {
                        "type": "Utilization",
                        "averageUtilization": int(policy.scale_up_threshold)
                    }
                }
            })
        elif policy.metric == ScalingMetric.MEMORY:
            hpa["spec"]["metrics"].append({
                "type": "Resource",
                "resource": {
                    "name": "memory",
                    "target": {
                        "type": "Utilization",
                        "averageUtilization": int(policy.scale_up_threshold)
                    }
                }
            })
    
    return hpa
Quick check3 questions · +5 XP each

Load testing and scaling

  1. Q1/3 Your service runs fine for the first few hours under normal load but gets OOM-killed after about six hours. Which test would have caught this?

    Answer & explanation

    Answer: C

    Soak tests run normal-to-moderate load for hours or days to surface leaks and drift. A stress test pushes load up until something breaks, but it is too short to show a slow memory leak.

  2. Q2/3 Your primary Postgres database is CPU-bound and you need relief this week while a longer-term plan is designed. What fits the lesson's guidance?

    Answer & explanation

    Answer: A

    Vertical scaling is recommended for primary databases and as a quick fix while horizontal scaling is planned. More app pods only send more queries to the same saturated database, and sharding is a long project, not a this-week fix.

  3. Q3/3 The web API config scales up by 3 pods with short cooldowns but scales down by 1 pod with a 15-minute cooldown. Why the asymmetry?

    Answer & explanation

    Answer: B

    Under-provisioning causes user-facing latency right away, so scale up fast and scale down cautiously to avoid flapping. The one-pod limit is a policy choice in the config, not a Kubernetes restriction.

Part III · Stage 3 of 4 · 3 min

Part III: Real-World Application#

Chapter 7: Case Studies#

7.1 How Netflix Handles Scale#

Netflix capacity planning
Scale
  • 280M+ subscribers (282.7M paid memberships at end of Q3 2024)
  • 15% of global internet traffic
  • Hundreds of microservices
  • Runs primarily on AWS
Key practices

1. Chaos engineering

  • Chaos Monkey: Randomly kills instances
  • Chaos Kong: Simulates region failure
  • FIT (Failure Injection Testing): Controlled fault injection
  • Result: Confidence in resilience

2. Predictive scaling

  • Know when shows release
  • Pre-scale ahead of big title launches
  • Analyze viewing patterns
  • Scale regions before prime time

3. Capacity testing

  • Regular load tests in production
  • Shift production traffic onto fewer instances to find real per-instance limits
  • Test well above expected peak
  • Find bottlenecks before customers do

4. Auto-scaling

  • Titus: Their container platform
  • Aggressive auto-scaling policies
  • Scale on load signals such as request rate and CPU
  • Can add large numbers of instances quickly

5. Regional distribution

  • Content in multiple AWS regions
  • CDN (Open Connect) for streaming
  • Failover between regions
  • No single region is critical

Lesson: At Netflix scale, capacity planning is a full-time job. They have dedicated teams for capacity analysis.

7.2 How Stripe Handles Scale#

Stripe capacity planning
Context
  • Processes billions in payments
  • Every request is high-value
  • Zero tolerance for downtime
  • Must scale for Black Friday, Cyber Monday

Illustrative: Stripe has written publicly about load shedding and rate limiters; the headroom figure, game-day multiplier and review cadence below are example practices, not published Stripe numbers.

Key practices

1. Metered overprovisioning

  • Always maintain 50%+ headroom
  • Cost of overprovisioning < cost of downtime
  • For payments, reliability > efficiency
  • Better to waste capacity than drop transactions

2. Load shedding

  • Prioritize payment completion over analytics
  • Graceful degradation under load
  • Non-critical features turn off automatically
  • Payment path is always protected

3. Regular game days

  • Simulate Black Friday in staging
  • Test at 3x expected peak
  • Practice runbooks
  • Find weaknesses before they matter

4. Multi-region

  • Active-active in multiple regions
  • Can shift traffic between regions
  • Used for capacity expansion
  • Also provides disaster recovery

5. Capacity committee

  • Weekly capacity review
  • Cross-functional (eng + ops + finance)
  • Review utilization, forecasts, plans
  • Make scaling decisions together

Lesson: For critical systems, overprovision. The cost of downtime far exceeds the cost of extra capacity.

Chapter 8: Common Mistakes#

Capacity planning anti-patterns
Mistake 1: Waiting for Problems

Wrong: "We'll scale when we see issues"

Problem:

  • Scaling takes time (minutes to hours)
  • By the time you see issues, users are affected
  • Reactive = outages

Right:

  • Know your limits from load testing
  • Track headroom continuously
  • Scale BEFORE you need to
Mistake 2: Only Measuring Averages

Wrong: "Average CPU is only 30%, we're fine!"

Problem:

  • Peak usage is what matters
  • Spikes cause outages, not averages
  • P99 utilization might be 95%

Right:

  • Track P99 utilization
  • Watch peak-to-average ratio
  • Plan for peaks, not averages
Mistake 3: Ignoring Dependencies

Wrong: "We can handle 10K RPS"

Problem:

  • But can your database?
  • Can your cache?
  • Can your third-party APIs?
  • You're only as fast as your slowest dependency

Right:

  • Test the entire stack
  • Know limits of all dependencies
  • Plan capacity for the bottleneck
Mistake 4: Not Testing Production Config

Wrong: "Load tests pass in staging!"

Problem:

  • Staging != production
  • Different data sizes
  • Different network topology
  • Different concurrency patterns

Right:

  • Load test in production (carefully)
  • Or use production-like staging
  • Mirror production traffic for testing
Mistake 5: Ignoring Auto-Scaling Lag

Wrong: "Auto-scaling will handle it"

Problem:

  • Auto-scaling takes 3-5 minutes
  • Spike hits before scale-up completes
  • Brief outage during scaling

Right:

  • Pre-scale for known events
  • Maintain baseline capacity for spikes
  • Auto-scaling for sustained increases
Quick check3 questions · +5 XP each

Capacity in practice

  1. Q1/3 The dashboard shows average CPU at 30% over the week, yet users see timeouts every evening. What is the most likely explanation?

    Answer & explanation

    Answer: D

    Spikes cause outages, not averages; a weekly 30% average can hide a nightly peak near 95%. Plan for peaks and watch the peak-to-average ratio rather than writing off the metric.

  2. Q2/3 Marketing is running a TV ad at 8 PM tonight that is expected to bring 4x traffic. You have auto-scaling. What should you do?

    Answer & explanation

    Answer: B

    Auto-scaling takes about 3-5 minutes, so a sudden spike lands before new capacity is ready; pre-scale for known events. A lower threshold still reacts only after the spike arrives and runs into the same lag.

  3. Q3/3 In the lesson's (illustrative) Stripe example, the payment platform keeps 50%+ headroom even though much of it sits idle. Why is that rational?

    Answer & explanation

    Answer: C

    For payments, reliability beats efficiency: the cost of downtime is far higher than the cost of extra capacity. That is a business trade-off, not a technical or regulatory limit.

Part IV · Stage 4 of 4 · 5 min

Part IV: Interview Preparation#

Chapter 9: Interview Tips#

9.1 Capacity Discussion Framework#

Discussing capacity in interviews

When asked "How would you ensure this system handles scale?":

  1. Estimate the load — "First, let me understand the scale:
    • How many users?
    • What's the request pattern?
    • What's the peak vs average ratio?

    Let me calculate the expected RPS..."

  2. Identify bottlenecks — "The likely bottlenecks are:
    • Database: Will it handle this QPS?
    • Memory: How much state per user?
    • Network: What's the payload size?

    Let me work through each..."

  3. Design for scale — "To handle this scale, I'd:
    • Add caching to reduce DB load
    • Shard the database by user_id
    • Use connection pooling
    • Deploy across multiple regions"
  4. Plan for growth — "To stay ahead of growth:
    • Maintain 30% headroom on all resources
    • Auto-scale based on CPU and latency
    • Load test regularly
    • Pre-scale for known events"
  5. Handle spikes — "For unexpected spikes:
    • Rate limiting protects the system
    • Load shedding drops non-critical work
    • Circuit breakers prevent cascade
    • Graceful degradation keeps core working"
❯ Say this

9.2 Key Phrases#

Capacity key phrases
On Measurement

"I'd establish a baseline by load testing to find where latency starts degrading. That's my sustainable capacity. Then I'd ensure we maintain at least 30% headroom below that limit."

On Bottlenecks

"The bottleneck is usually the database. At 1000 RPS with 10ms average query time, we need 10 concurrent connections minimum. I'd use connection pooling and add read replicas if needed."

On Little's Law

"Using Little's Law: if we have 500 RPS and 100ms response time, we need 50 concurrent handlers. With 4 workers per pod, that's 12.5 pods. Keeping 30% headroom means running at 70%: 12.5 / 0.7 ≈ 18 pods minimum."

On Auto-Scaling

"Auto-scaling reacts to load, but it's not instant. For predictable events like Black Friday, I'd pre-scale the night before. Auto-scaling handles the unexpected variations."

On Load Testing

"I'd run regular load tests at 2x expected peak. This finds bottlenecks before customers do. The goal isn't just to pass, but to understand where the system breaks."

Chapter 10: Practice Problems#

Practice · problem 1

Problem 1: E-Commerce Peak Planning#

Scenario: Your e-commerce site normally handles 500 RPS. Black Friday is coming with expected 10x traffic.

Questions:

  1. How would you prepare for 5,000 RPS?
  2. What needs to scale and by how much?
  3. What's your testing strategy?
Click for approach
  1. Preparation (6-8 weeks before):

    • Load test current system to find breaking point
    • Identify bottlenecks (probably database)
    • Plan and implement scaling
    • Schedule pre-scaling for the night before
  2. What to scale:

    • Web tier: 10x pods (with auto-scaling headroom)
    • Database: Add read replicas, warm connection pools
    • Cache: 10x cache size to handle increased working set
    • CDN: Pre-warm with popular content
  3. Testing:

    • Load test at 10x in staging
    • Shadow traffic test in production
    • Have runbooks for manual scaling
    • War room staffed during peak

Practice · problem 2

Problem 2: Database Scaling#

Scenario: Your database handles 2,000 QPS. You're at 1,500 QPS and growing 10% monthly.

Questions:

  1. When will you hit capacity?
  2. What are your options?
  3. How would you buy time?
Click for approach
  1. Time to capacity:

    • Month 1: 1,500 QPS (75% utilized)
    • Month 2: 1,650 QPS (82.5%)
    • Month 3: 1,815 QPS (91%)
    • Month 4: 2,000 QPS (100%) - CRITICAL
    • Action needed by month 2
  2. Options:

    • Vertical: Upgrade to larger instance (quick fix)
    • Read replicas: Offload read traffic (medium effort)
    • Caching: Reduce queries (high impact)
    • Sharding: Long-term solution (high effort)
  3. Buying time:

    • Add aggressive caching (1-2 weeks)
    • Optimize slow queries (ongoing)
    • Add read replica (2-4 weeks)
    • Plan sharding for 6-month horizon

Chapter 11: Sample Interview Dialogue#

Transcript
Interviewer

"You're designing a food delivery app. How do you plan for capacity during dinner rush?"

You

"Great question. Let me think through the load characteristics first.

Understanding the pattern:"

WorksheetTraffic pattern
  • Normal: 1,000 orders/minute
  • Dinner rush (6-8 PM): 5x = 5,000 orders/minute
  • Friday dinner: 7x = 7,000 orders/minute
  • Super Bowl Sunday: 20x = 20,000 orders/minute
Each order involves
  • 5-10 API calls (browse, add to cart, checkout)
  • 1 database write (order creation)
  • 2-3 external calls (payment, restaurant, driver)
  • Plus browsing by visitors who don't order (several sessions per order placed)
  • Estimate: 50-100 API calls per order placed, counting that browsing
Peak API load
  • 7,000 × 100 = 700,000 API calls/minute = ~12,000 RPS
Transcript
Interviewer

"How would you handle that?"

You

"I'd design with the peak in mind and auto-scale for variations.

For the API tier:"

WorksheetAPI tier sizing
  • Using Little's Law: 12,000 RPS × 0.1s = 1,200 concurrent requests
  • With 20 workers per pod: 1,200/20 = 60 pods minimum
  • Add 50% extra capacity: 90 pods (60/90 → ~33% headroom, above the 30% rule)
  • Auto-scale between 20 pods (baseline) and 150 pods (max)

"For the database:"

WorksheetDatabase sizing
  • 7,000 writes/minute = 117 writes/second
  • Reads are probably 10x writes = 1,170 reads/second
  • Use read replicas for menu/restaurant queries
  • Shard orders table by region for write scaling
  • Cache frequently accessed data (menus, restaurant info)
Transcript
Interviewer

"What about Super Bowl Sunday at 20x?"

You

"For exceptional events, I wouldn't rely on auto-scaling alone.

Pre-event preparation:"

Pre-event preparation
  1. Week before: Load test at 25x (leave margin)
  2. Night before: Pre-scale to 50% of expected peak
  3. During event: Auto-scale handles variations
  4. Have war room staffed
  5. Pre-warm caches with popular restaurants
If we still hit limits
  • Graceful degradation: Disable non-essential features
  • Rate limiting: Queue orders instead of rejecting
  • Geographic limiting: Prioritize areas we can serve
Transcript
Interviewer

"Good systematic approach. How do you know if your planning worked?"

You

"I'd set up clear success metrics:

During the event:

  • Order success rate > 99%
  • Checkout latency < 3 seconds
  • No capacity alerts firing
  • Auto-scaling responded within 3 minutes

After the event:

  • Review peak utilization (should be < 70%)
  • Identify any bottlenecks that appeared
  • Cost analysis: did we over/under provision?
  • Update forecasting model with actual data

This becomes input for next year's planning."

Summary#

Day 4 key takeaways
Capacity planning cycle
  • Measure: Know your current capacity and utilization
  • Forecast: Predict future demand
  • Identify: Find bottlenecks before they hit
  • Plan: Decide how and when to scale
  • Validate: Test your assumptions
Key metrics
  • Headroom = (Max Capacity - Current) / Max Capacity
  • Maintain at least 30% headroom
  • Track P99 utilization, not just averages
  • Little's Law: L = λ × W
Load testing
  • Smoke: Verify basic functionality
  • Load: Test normal production traffic
  • Stress: Find the breaking point
  • Spike: Test sudden traffic jumps
  • Soak: Find issues that emerge over time
Scaling strategies
  • Vertical: Bigger machines (simple, limited)
  • Horizontal: More machines (complex, unlimited)
  • Auto-scaling: React to load (not instant!)
  • Pre-scaling: Prepare for known events
Common bottlenecks
  • Database (connections, queries)
  • Application CPU
  • Memory (especially caches)
  • External dependencies
  • Network bandwidth
Key insight

The best time to scale is BEFORE you need to. Know your limits. Watch your headroom. Plan ahead.

Further Reading#

Books:

  • Book

    "The Art of Capacity Planning" by John Allspaw

  • Book

    "Release It!" by Michael Nygard - Capacity patterns

Tools:

  • Read

    k6: Modern load testing

  • Read

    Locust: Python-based load testing

  • Read

    Grafana + Prometheus: Capacity monitoring

  • Read

    AWS Auto Scaling / Kubernetes HPA

Articles:

  • Read

    Netflix: "Lessons from Building Observability Tools at Netflix"

  • Book

    Google SRE Book: Chapter 18, "Software Engineering in SRE" (the Auxon capacity-planning case study)

  • Docs

    Stripe: "Scaling Your API with Rate Limiters"

You now have the full operational toolkit: Define health. See health. Maintain health through change. Ensure health under load. Tomorrow: what to do when health fails.

this lesson in shorts · 2

play them all →

One idea from this lesson in about a minute each, made for your phone.

End of Day 04 · Capacity Planning
Finish Day 4Stages clear as you read them (0/4 so far). Skipping ahead doesn't count: marking complete unlocks after ~6 more min of reading.

Go deeper · reading trail

all reading →

Real write-ups from the teams that built these systems. Each opens on the publisher's site in a new tab.

MetaPaper · ~40 min

Kraken: Leveraging Live Traffic Tests to Identify and Resolve Resource Utilization Bottlenecks in Large Scale Web Services

Kaushik Veeraraghavan, Justin Meza et al. · 2016
What
Facebook's system for load testing with real user traffic.
Key idea
Shift live traffic onto a cluster or region until it reaches a target load, watch health metrics, and record the bottlenecks found along the way.
Why here
The load-testing chapter of this lesson without synthetic traffic: measure real capacity by steering production load.
taught here: Load testing
Dream11Blog post · ~8 min

To Scale In Or Scale Out? Here's How We Scale at Dream11

2021
What
How Dream11 scales for traffic that spikes around each match's toss and start.
Key idea
With 5.5 million concurrent users and 100 million requests a minute at peak, scaling follows the match timeline (before, during, after) rather than waiting for autoscaling.
Why here
Forecast-driven scaling from this lesson: when you know when the spike comes, provision ahead of it.
taught here: Forecasting demand
JioHotstarBlog post · ~5 min

T For Tsunami : Dealing with traffic spikes

Akash Saxena · 2017
What
Hotstar's account of a capacity miss during an India-Pakistan match.
Key idea
The fleet was scaled for 7M concurrent users, but a sudden login spike hit a database that had not been scaled in proportion, since logins cannot be cached.
Why here
The bottleneck lesson in one story: capacity plans must scale every tier on the hot path, not just the stateless front.
taught here: Identifying bottlenecks

New lessons by email

New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.

Discussion · …

Public: everyone reading this lesson sees comments. Be kind and keep it on the material.

Sign in to ask a question or answer one. Reading stays open to everyone.
loading the discussion…