Free lesson · System Design Interviews
Handling Traffic Spikes - Building Systems That Survive the Unexpected
Here's a story from my time at Stripe. Black Friday 2019. We'd planned for 3x normal traffic. We got 7x. Not because our estimates were wrong - our merchants' marketing campaigns were more successful than they expected. Within 12 minutes, the payment processing latency went from 200ms to 8 seconds. Checkout pages started timing out. Revenue was evaporating for our merchants at $40,000 per minute.
We survived because of a pattern we'd built specifically for this scenario: backpressure propagation with graceful degradation. The system didn't try to process everything at full quality. It shed load intelligently, prioritizing payment completion over analytics, receipts, and webhooks. Those secondary operations queued up and drained over the next 6 hours. Every payment went through. Nothing was lost.
That experience is why I now ask every senior candidate: "Your system is designed for 10,000 requests per second. At 2 AM, it suddenly receives 100,000 requests per second. What happens?" The answer reveals whether they've ever operated a system under real pressure.
Why Spikes Are Different From Steady Growth
Steady traffic growth gives you time. You see the metrics trending up over weeks, you provision more capacity, you're fine. Spikes don't give you time.
| Spike Type | Warning Time | Examples |
|---|---|---|
| Predictable | Days/weeks | Black Friday, product launch, Super Bowl |
| Semi-predictable | Hours | Breaking news, celebrity endorsement, viral content |
| Unpredictable | Zero | DDoS attack, cascading failure, sudden API surge |
Your architecture needs strategies for all three. Predictable spikes use pre-scaling. Semi-predictable spikes use auto-scaling. Unpredictable spikes use backpressure and load shedding - because there's no time to add capacity.
Strategy 1: Rate Limiting - The Front Door Bouncer
Rate limiting is the first line of defense. It rejects excess traffic before it reaches your application servers, preventing cascading overload.
Rate Limiting Algorithms
flowchart LR
subgraph ALGORITHMS["Rate Limiting Algorithms"]
direction TB
TB["Token Bucket<br/>Allows bursts up to bucket size<br/>Refills at steady rate"]
SW["Sliding Window<br/>Counts requests in rolling window<br/>Smooth, no burst edges"]
FW["Fixed Window<br/>Counts per time window<br/>Simple but boundary spikes"]
LC["Leaky Bucket<br/>Processes at fixed rate<br/>Queues excess, drops overflow"]
end
Four rate limiting algorithms. Token bucket allows bursts up to the bucket capacity and refills at a steady rate. Sliding window counts requests in a rolling time window for smooth limiting. Fixed window counts per fixed time period but can allow burst at window boundaries. Leaky bucket processes at a fixed rate, queuing excess and dropping overflow.
Token Bucket - The Interview Default
The token bucket is the most commonly discussed algorithm and the best default answer:
- The bucket starts with N tokens (the burst capacity)
- Each request consumes one token
- Tokens refill at a steady rate (the sustained rate)
- When the bucket is empty, requests are rejected
class TokenBucket:
def __init__(self, rate, capacity):
self.rate = rate # tokens per second
self.capacity = capacity # max burst size
self.tokens = capacity
self.last_refill = time.time()
def allow(self):
now = time.time()
elapsed = now - self.last_refill
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.last_refill = now
if self.tokens >= 1:
self.tokens -= 1
return True
return False
Multi-Level Rate Limiting
Don't rate limit at just one layer:
| Layer | Rate Limit | Purpose |
|---|---|---|
| CDN / Edge | 10,000 req/sec per IP | Block DDoS, abusive scrapers |
| API Gateway | 1,000 req/sec per API key | Enforce plan limits |
| Application | 100 req/sec per user per endpoint | Prevent accidental abuse |
| Database | Connection pool limits | Prevent connection exhaustion |
Interview: What to say: "I'd implement rate limiting at three layers. At the edge, Cloudflare rate limits by IP - 10K requests per second per IP, which blocks DDoS and scrapers. At the API gateway, rate limiting by API key enforces plan limits and prevents any single client from overwhelming the system. At the application layer, per-user per-endpoint limits prevent accidental tight loops in client code. The edge limit protects the infrastructure, the gateway limit protects the business model, and the application limit protects the user experience."
Strategy 2: Load Shedding - Dropping Low-Priority Work
When rate limiting isn't enough - when even legitimate traffic exceeds capacity - load shedding drops lower-priority work to protect higher-priority work.
Priority Classification
flowchart TD
REQUEST["Incoming Request"] --> CLASSIFY{"Priority<br/>Classification"}
CLASSIFY -->|"P0: Critical"| P0["Payment Processing<br/>Login/Auth<br/>Order Submission<br/>→ Always process"]
CLASSIFY -->|"P1: Important"| P1["Feed Loading<br/>Search Results<br/>Product Pages<br/>→ Process if capacity allows"]
CLASSIFY -->|"P2: Deferrable"| P2["Analytics Events<br/>Recommendation Refresh<br/>Email Notifications<br/>→ Queue for later"]
CLASSIFY -->|"P3: Droppable"| P3["Prefetch/Warmup<br/>Non-critical Webhooks<br/>Background Sync<br/>→ Drop during overload"]
Load shedding priority classification. P0 critical requests like payments, auth, and orders are always processed. P1 important requests like feed loading and search are processed if capacity allows. P2 deferrable requests like analytics and notifications are queued for later processing. P3 droppable requests like prefetch and background sync are dropped entirely during overload.
Implementation: The Circuit-Based Approach
class LoadShedder:
def __init__(self, capacity_threshold=0.85):
self.threshold = capacity_threshold
def should_process(self, request):
current_load = self.get_system_load() # CPU, queue depth, latency
if request.priority == "P0":
return True # Always process critical requests
if current_load < 0.70:
return True # Under 70% capacity - process everything
if current_load < 0.85:
return request.priority in ("P0", "P1") # Shed P2 and P3
if current_load < 0.95:
return request.priority == "P0" # Only critical requests
# Over 95% - even some P0 might get queued
return request.priority == "P0" and random.random() < 0.5
Warning: Load shedding must return fast. A shed request should return
503 Service Unavailablein under 10ms - not sit in a queue. The whole point is to free up resources. If shed requests consume processing time before being rejected, shedding doesn't help.
Strategy 3: Auto-Scaling - Adding Capacity Dynamically
For spikes that last longer than a few minutes, the system should automatically add compute capacity.
Scaling Signals
| Signal | What It Measures | Best For |
|---|---|---|
| CPU utilization | Processing saturation | Compute-heavy workloads |
| Request queue depth | Demand exceeding capacity | Request-heavy workloads |
| Response latency (p99) | User experience degradation | Any web service |
| Custom metric (active connections) | Domain-specific saturation | WebSocket servers, streaming |
Scaling Speed
The critical question is how fast new capacity comes online:
| Approach | Time to New Capacity | Cost |
|---|---|---|
| Pre-warmed standby instances | 10-30 seconds | High (paying for idle capacity) |
| Container scaling (ECS, K8s) | 30-90 seconds | Medium |
| VM scaling (EC2 Auto Scaling) | 3-5 minutes | Low |
| New database replicas | 15-60 minutes | N/A (too slow for spike response) |
flowchart LR
SPIKE["Traffic Spike<br/>Detected"] --> IMMEDIATE["Immediate (0-30s)<br/>→ Rate limit<br/>→ Load shed<br/>→ Use standby instances"]
IMMEDIATE --> SHORT["Short-term (30s-5min)<br/>→ Scale containers<br/>→ Scale stateless services"]
SHORT --> MEDIUM["Medium-term (5-60min)<br/>→ Scale VMs<br/>→ Add cache capacity<br/>→ Add read replicas"]
Three-phase spike response timeline. In the first 30 seconds, use immediate responses: rate limiting, load shedding, and activating standby instances. From 30 seconds to 5 minutes, scale containers and stateless services. From 5 to 60 minutes, scale VMs, add cache capacity, and spin up read replicas. Each phase buys time for the next.
Tip: In interviews, mention the time dimension: "Auto-scaling takes 60-90 seconds to bring new containers online. For the first 60 seconds of a spike, we rely on rate limiting and load shedding to protect the system. Once new instances are healthy and taking traffic, the shedding eases off. This layered response means we handle the first minute with defensive measures and subsequent minutes with increased capacity."
Strategy 4: Backpressure - Slowing Down Instead of Failing
Backpressure is the principle that when a downstream service is overwhelmed, it should signal upstream to slow down rather than accepting requests it can't process.
Without Backpressure
sequenceDiagram
participant C as Client
participant API as API Server
participant Q as Queue
participant W as Worker
participant DB as Database
Note over C,DB: Without backpressure - cascading failure
C->>API: 10,000 req/sec
API->>Q: 10,000 msg/sec
Q->>W: Workers pull as fast as they can
W->>DB: 10,000 writes/sec
Note over DB: DB handles 5,000/sec max<br/>Connection pool exhausted
DB--xW: Connection timeout
W--xQ: Messages pile up (queue grows unbounded)
Q--xAPI: Queue memory exhausted → crash
API--xC: 500 Internal Server Error
Cascading failure without backpressure. The client sends 10,000 requests per second. The API server pushes everything to the queue. Workers consume as fast as possible and slam the database with 10,000 writes per second. The database can only handle 5,000 per second, so connections time out. Messages pile up in the queue until memory is exhausted and the queue crashes. The API server returns 500 errors. Everything fails.
With Backpressure
sequenceDiagram
participant C as Client
participant API as API Server
participant Q as Queue
participant W as Worker
participant DB as Database
Note over C,DB: With backpressure - controlled degradation
C->>API: 10,000 req/sec
API->>Q: Check queue depth
Note over Q: Queue depth > threshold
Q-->>API: Backpressure signal
API-->>C: 429 Too Many Requests (for excess traffic)
API->>Q: 5,000 msg/sec (admitted traffic)
Q->>W: Workers pull at sustainable rate
W->>DB: 5,000 writes/sec (within capacity)
Note over DB: DB operates normally
Controlled degradation with backpressure. The client sends 10,000 requests per second. The API server checks the queue depth, finds it above threshold, and returns 429 responses for excess traffic while admitting only 5,000 messages per second to the queue. Workers consume at a sustainable rate. The database operates within its capacity. The system is slower for some clients but never crashes.
Implementing Backpressure
Backpressure signals include:
- Queue depth thresholds: If the queue has more than N messages, stop accepting new ones
- Response time thresholds: If downstream latency exceeds M milliseconds, slow the admission rate
- Connection pool saturation: If X% of database connections are in use, reject new requests
- HTTP 429 (Too Many Requests): The standard response code with a
Retry-Afterheader
Interview: What to say: "I'd implement backpressure at the queue layer. The API server checks the Kafka consumer lag before accepting new writes. If the lag exceeds 100,000 messages - meaning workers are more than 10 seconds behind - the API server starts returning 429 with a Retry-After header. This prevents the queue from growing unbounded and gives the workers time to catch up. The client retries with exponential backoff. The system degrades gracefully instead of cascading into failure."
Strategy 5: Graceful Degradation - The Feature Toggle Approach
When the system is under extreme load, turn off non-essential features to free up resources for the essential ones.
Feature Priority Map
flowchart TB
NORMAL["Normal Load<br/>All Features On"] --> ELEVATED["Elevated Load (70-85%)<br/>Disable: personalized recommendations<br/>Serve: cached/generic recommendations"]
ELEVATED --> HIGH["High Load (85-95%)<br/>Disable: search suggestions, analytics<br/>Serve: simplified search, skip tracking"]
HIGH --> CRITICAL["Critical Load (95%+)<br/>Disable: images, comments, likes<br/>Serve: text-only core functionality"]
style NORMAL fill:#4CAF50,color:#fff
style ELEVATED fill:#FFC107,color:#000
style HIGH fill:#FF9800,color:#fff
style CRITICAL fill:#f44336,color:#fff
Graceful degradation across four load levels. At normal load, all features are enabled. At elevated load between 70 and 85 percent, personalized recommendations are replaced with cached generic ones. At high load between 85 and 95 percent, search suggestions and analytics tracking are disabled. At critical load above 95 percent, images, comments, and likes are disabled, serving only text-based core functionality.
Real-world example from Amazon: During peak Prime Day traffic, Amazon disables or simplifies features progressively. The product page might lose the "customers also bought" recommendations section, the review section might show fewer reviews, and the "compare with similar items" widget might disappear entirely. The core function - seeing the product, seeing the price, adding to cart, and checking out - is always protected.
Tip: Design your system with feature flags that can be toggled remotely via a configuration service (like LaunchDarkly or a custom feature flag system backed by etcd). During a spike, an on-call engineer - or an automated system - toggles non-essential features off. This is faster than any code deployment and can be reversed instantly.
Putting It All Together - The Spike Response Playbook
flowchart TD
SPIKE["🚨 Traffic Spike Detected"] --> T1["0-10 seconds"]
T1 --> RATE["Rate limiting activates<br/>Edge blocks abusive traffic"]
T1 --> SHED["Load shedding kicks in<br/>P2/P3 requests dropped"]
SPIKE --> T2["10-60 seconds"]
T2 --> DEGRADE["Feature toggles fire<br/>Non-essential features disabled"]
T2 --> STANDBY["Standby instances activated"]
SPIKE --> T3["1-5 minutes"]
T3 --> SCALE["Auto-scaling provisions<br/>new containers"]
T3 --> CACHE["Cache warming for<br/>hot content"]
SPIKE --> T4["5+ minutes"]
T4 --> RESTORE["Features gradually restored<br/>as capacity stabilizes"]
T4 --> REVIEW["On-call reviews<br/>spike cause and duration"]
The spike response playbook in four phases. In the first 10 seconds, rate limiting and load shedding activate automatically. From 10 to 60 seconds, feature toggles disable non-essential features and standby instances come online. From 1 to 5 minutes, auto-scaling provisions new containers and caches warm for hot content. After 5 minutes, features are gradually restored as capacity stabilizes and the on-call team reviews the cause.
Key Takeaways
-
Rate limiting is your first defense. Implement at multiple layers - edge, gateway, application - each protecting against different threat profiles. Token bucket is the default algorithm for interviews.
-
Load shedding preserves critical functionality. Classify requests by priority and drop low-priority work first. P0 requests (payments, auth) are always processed. P3 requests (prefetch, background sync) are dropped during overload.
-
Auto-scaling takes time - bridge the gap. Container scaling takes 60-90 seconds. VM scaling takes 3-5 minutes. Use rate limiting and load shedding during the first minutes while new capacity spins up.
-
Backpressure prevents cascading failures. When downstream services are overwhelmed, signal upstream to slow down. The alternative - accepting requests you can't process - leads to queue exhaustion, timeouts, and system-wide failure.
-
Graceful degradation trades features for stability. Design with feature flags that can disable non-essential features at runtime. During a spike, the system serves a simpler but functional experience rather than failing entirely.
-
Layer your defenses by time horizon. Seconds: rate limiting and load shedding. Minutes: auto-scaling and feature degradation. Hours: manual capacity planning and traffic rerouting. Each layer buys time for the next.
Exercise: Exercise 1: You operate a ticket-selling platform. A major concert goes on sale at noon and you expect 500,000 users to attempt purchases simultaneously for 10,000 available tickets. Design the traffic spike strategy. How do you handle the initial thundering herd? How do you prevent overselling? What does the user experience look like for the 490,000 people who won't get tickets?
Exercise 2: Your API serves both free-tier and paid-tier customers. During a traffic spike, you want to protect paid-tier customer experience at the expense of free-tier. Design a multi-tier rate limiting and load shedding system that gives paid customers priority access during overload while still serving free-tier customers when capacity allows.
Exercise 3: Design an auto-scaling policy for a service that has predictable daily traffic patterns (2x during business hours) and unpredictable spikes (10x for 5-15 minutes). The service runs on Kubernetes. How do you configure the Horizontal Pod Autoscaler? What metric do you scale on? How do you handle the cold-start problem for new pods? What's the cost difference between reactive scaling and keeping warm standby pods?