Identifying Bottlenecks
What Is a Bottleneck?
A bottleneck is any component that limits overall system throughput. The system can only perform as fast as its slowest component. A single slow database query can make an entire API endpoint slow, regardless of how fast the application server is.
Profiling
Application-level profiling:
- CPU profiling: Identify functions consuming the most CPU cycles (flame graphs)
- Memory profiling: Find memory leaks, excessive allocations, garbage collection pressure
- I/O profiling: Detect slow disk reads/writes, excessive file operations
- Tools: pprof (Go), async-profiler (Java), py-spy (Python), clinic.js (Node.js)
Database profiling:
- Slow query log: Log queries exceeding a time threshold (e.g., > 200ms)
- EXPLAIN ANALYZE: Show query execution plan — find sequential scans, missing indexes
- pg_stat_statements (PostgreSQL): Top queries by total time, calls, rows
- Performance Schema (MySQL): Lock waits, table scans, buffer pool hit ratio
Network profiling:
- Latency measurement: traceroute, mtr, ping between services
- Packet capture: Wireshark for protocol-level analysis
- DNS resolution time: Often overlooked — slow DNS adds latency to every request
Monitoring
Key metrics to track:
| Metric | What it reveals | Tool examples |
|---|---|---|
| CPU utilization | Compute bottleneck | CloudWatch, Datadog, Prometheus |
| Memory usage | Memory leaks, insufficient RAM | CloudWatch, Grafana |
| Disk I/O (IOPS) | Storage bottleneck | CloudWatch, iostat |
| Network throughput | Bandwidth saturation | CloudWatch, iftop |
| Request latency (P50, P95, P99) | Tail latency issues | CloudWatch, New Relic |
| Error rate | Failing requests | CloudWatch, Sentry |
| Queue depth | Processing backlog | SQS metrics, Kafka lag |
| Connection pool usage | DB connection exhaustion | PgBouncer stats, HikariCP metrics |
The golden signals (Google SRE):
- Latency: Time to serve a request
- Traffic: Requests per second
- Errors: Rate of failed requests
- Saturation: How "full" is the resource (CPU, memory, connections)
Load Testing
What it reveals:
- Breaking point of the system under realistic load
- Which component degrades first
- Whether auto-scaling kicks in properly
Tools: k6, Locust, Apache JMeter, Artillery
Test types:
- Load test: Simulate expected peak traffic
- Stress test: Push beyond expected peak to find breaking point
- Soak test: Sustained load over hours to find memory leaks, connection exhaustion
- Spike test: Sudden traffic burst to test auto-scaling response
Load testing process:
- Define realistic user journeys (login → browse → add to cart → checkout)
- Set baseline metrics (response time, error rate, throughput)
- Gradually increase load
- Monitor all four golden signals
- Identify the first component to degrade — that's your bottleneck
- Fix it, then repeat
Common Bottleneck Patterns
Database Query Bottlenecks
Symptoms: High database CPU, slow query times, connection pool exhaustion
Common causes:
- Missing indexes on frequently queried columns
- N+1 query patterns (1 query to fetch list + N queries for each item)
- Full table scans on large tables
- Lock contention (long-running transactions blocking others)
- Unoptimized JOINs across large tables
Diagnosis:
-- PostgreSQL: Find slow queries
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;
-- Find missing indexes
SELECT schemaname, relname, seq_scan, seq_tup_read
FROM pg_stat_user_tables
WHERE seq_scan > 100
ORDER BY seq_tup_read DESC;
Network Latency
Symptoms: High P99 latency but normal P50, intermittent timeouts
Common causes:
- Cross-region service calls (100-300ms added per hop)
- DNS resolution delays
- TLS handshake overhead (especially for new connections)
- Load balancer added latency
- Insufficient connection pooling (connection setup cost)
Diagnosis:
- Distributed tracing (Jaeger, Zipkin, X-Ray) shows per-hop latency
- Compare P50 vs P99 — large gap suggests tail latency from network
- DNS lookup timing in logs
Real example:
A microservice calls 5 downstream services sequentially. Each adds 50ms network latency. Total: 250ms just from network. Parallelizing these calls reduces it to ~60ms (max of 5 parallel calls).
Single Points of Failure (SPOF)
Symptoms: System fails completely when one component goes down
Common SPOFs:
- Single database instance (no replica, no failover)
- Single application server behind DNS
- Single Redis instance for caching
- Single message broker
- Shared filesystem (NFS)
Detection:
- Architecture review: draw the system, identify components with no redundancy
- Chaos engineering: randomly kill components and observe impact
- AWS Well-Architected Tool reviews
Memory Bottlenecks
Symptoms: High memory usage, frequent garbage collection, OOM kills
Common causes:
- Memory leaks (objects allocated but never freed)
- Large in-memory data structures (unbounded caches, full result sets)
- Insufficient heap size for JVM applications
- Buffer bloat (excessive buffering in network stacks)
Diagnosis:
- Heap dumps (jmap, VisualVM for Java)
- Memory profiling (Valgrind, heaptrack)
- Container OOM events in Kubernetes logs
- Swap usage — if swap is active, memory is exhausted
CPU Bottlenecks
Symptoms: High CPU utilization, slow request processing
Common causes:
- CPU-intensive computation in request path (image processing, encryption)
- Inefficient algorithms (O(n²) where O(n) is possible)
- Excessive garbage collection
- Lock contention causing busy-waiting
- JSON serialization/deserialization overhead
Diagnosis:
- CPU profiling (flame graphs show where CPU time is spent)
- Compare CPU usage across instances — if uneven, load balancing issue
- Thread dumps for Java — find threads in BLOCKED state
Connection Pool Exhaustion
Symptoms: Timeout errors, "too many connections" errors, increasing latency
Common causes:
- Connection leaks (opening connections but not closing them)
- Insufficient pool size for traffic volume
- Long-held connections (slow queries holding connections)
- Too many services sharing one pool
Diagnosis:
- Monitor active vs idle connections in pool
- Check for connections in ESTABLISHED state that aren't being used
- Application logs for connection timeout errors
Fix: Connection pooling (PgBouncer for PostgreSQL, HikariCP for Java) with properly tuned pool size. Formula: pool_size = (core_count * 2) + effective_spindle_count (for disk-bound workloads).
Resolving Bottlenecks
Amdahl's Law
Formula: Speedup = 1 / ((1 - P) + P/S)
Where:
- P = fraction of execution time that can be parallelized
- S = speedup factor from parallelization
Key insight: If 50% of your system is serial (P = 0.5), maximum speedup from parallelization is 2×, regardless of how many cores you add. The serial portion dominates.
Implication for bottleneck resolution:
- Optimizing a component that accounts for 10% of latency gives at most 10% improvement
- Optimizing a component that accounts for 90% of latency gives up to 90% improvement
- Always identify the biggest bottleneck first — that's where optimization effort has the most impact
Example:
- API endpoint takes 200ms total
- Database query: 150ms (75%)
- Application logic: 40ms (20%)
- Network serialization: 10ms (5%)
- Optimizing application logic by 50% saves 20ms (10% total improvement)
- Optimizing database query by 50% saves 75ms (37.5% total improvement)
Resolution Patterns
**1. Caching
What to cache:
- Computed results that are expensive to generate
- Database query results for read-heavy data
- API responses with predictable access patterns
- Session data, user preferences, configuration
Cache layers:
- Application-level (in-process LRU): < 1ms access, limited by single instance memory
- Distributed cache (Redis/Memcached): 1-5ms access, shared across instances
- CDN cache: 10-50ms access, for static/semi-static content
- Database query cache: built into DB engine, limited by buffer pool size
Cache strategies:
- Cache-aside (lazy loading): App checks cache first, on miss reads from DB and populates cache
- Write-through: Writes go to cache and DB simultaneously
- Write-behind (write-back): Writes go to cache, async flush to DB
- Read-through: Cache handles DB reads internally
Cache pitfalls:
- Cache stampede: many requests hit DB simultaneously when cache expires
- Thundering herd: same issue at larger scale
- Stale data: cache serves outdated information
- Memory pressure: cache evicts useful data
**2. Read Replicas
When to use:
- Read-to-write ratio > 10:1
- Database CPU bottleneck from read queries
- Need geographic distribution of reads
Implementation:
- Route reads to replicas via connection string or proxy
- Handle replication lag (read-your-writes consistency via session sticky reads)
- Monitor replica lag and failover if primary fails
**3. Connection Pooling
When to use:
- Database connection count hitting limits
- Frequent connection setup/teardown overhead
- Multiple services sharing database connections
Implementation:
- PgBouncer (PostgreSQL): transaction-level pooling for maximum efficiency
- HikariCP (Java): high-performance connection pool
- ProxySQL (MySQL): connection pooling + query routing
Pool sizing: Too few connections → requests wait. Too many → context switching overhead, memory waste. Start with (CPU cores * 2) + disk spindles and adjust based on monitoring.
**4. Async Processing
When to use:
- Task doesn't need to complete before responding to user
- Task is slow (email sending, image processing, report generation)
- Task is non-critical (analytics, audit logging)
Implementation:
- Message queue (SQS, Kafka, RabbitMQ) between request handler and worker
- Workers process tasks independently
- User gets immediate response; background workers handle slow operations
Example flow:
Request → App → Queue (immediate response to user)
→ Worker 1 (process image)
→ Worker 2 (send email)
→ Worker 3 (update analytics)
**5. Denormalization
When to use:
- Complex JOINs are slow and frequent
- Read-heavy system where writes are less frequent
- Specific query patterns that require data from multiple tables
Trade-off: Write complexity increases (must maintain denormalized data), but read performance improves dramatically.
Performance Testing Framework
Load Testing:
- Simulate expected peak traffic (e.g., 10K RPS)
- Measure response time, error rate, resource utilization
- Verify auto-scaling works as expected
- Run for 30-60 minutes to catch gradual degradation
Stress Testing:
- Push beyond expected peak (e.g., 2× expected)
- Find the breaking point — when does the system fail?
- Identify which component fails first
- Test recovery behavior after overload
Soak Testing:
- Sustained load for hours (4-24 hours)
- Catches memory leaks, connection exhaustion, disk fill
- Reveals gradual performance degradation
- Most overlooked test type — critical for production readiness
Capacity Planning:
- Based on load test results, calculate cost per 1K RPS
- Project growth: if traffic doubles in 6 months, plan infrastructure accordingly
- Leave 30-50% headroom for traffic spikes
- Set up alerts for capacity thresholds (80% CPU, 70% memory, etc.)
Real Example: Bottleneck Analysis for a Social Media Feed
System Overview
A social media feed service where users can post updates and see a timeline of posts from people they follow. Target: 10M daily active users, 500M feed loads/day.
Initial architecture:
- API gateway → Application servers → PostgreSQL (primary + 1 read replica)
- Redis cache for sessions
- S3 for media storage
Identifying the Bottleneck
Load test results at 50K RPS:
- API response time P50: 120ms, P99: 2,800ms
- Error rate: 3.2% (mostly 503 timeouts)
- Database CPU: 98%
- Application CPU: 35%
- Redis hit ratio: 45%
Analysis: The database is clearly the bottleneck. The feed query requires joining the follows table with the posts table, aggregating by recency, and returning 50 posts. With 10M users and millions of follows, this JOIN is extremely expensive.
Amdahl's Law calculation:
- Feed query: 800ms (65% of total latency)
- Application logic: 300ms (25%)
- Network/serialization: 120ms (10%)
- Optimizing application logic by 50% saves 150ms (12% total improvement)
- Optimizing feed query by 50% saves 400ms (32% total improvement)
- Focus on the feed query.
Resolution: Fan-out on Write
Problem: The "fan-out on read" approach computes each user's feed on-demand by joining follows + posts. This doesn't scale.
Solution: Fan-out on write — pre-compute each user's feed when a post is created.
Implementation:
Post created → Write to posts table
→ Query user's followers
→ Push post_id to each follower's feed list (Redis sorted set)
→ Feed read: ZREVRANGE on user's feed list (O(log N) operation)
Redis feed structure:
Key: feed:{user_id}
Type: Sorted Set
Score: timestamp
Value: post_id
Feed read:
ZREVRANGE feed:12345 0 49 # Get 50 most recent posts
Results After Optimization
- Feed query time: 800ms → 8ms (100× improvement)
- API P99 latency: 2,800ms → 180ms
- Database CPU: 98% → 25%
- Redis memory: increased by 120GB (trade-off)
- Write amplification: each post triggers N writes (one per follower)
Remaining Bottlenecks & Further Optimization
New bottleneck: Celebrity problem
- A celebrity with 10M followers — writing one post triggers 10M Redis writes
- Solution: Hybrid approach — fan-out on write for normal users, fan-out on read for celebrities (compute feed on-demand, but cache the result)
New bottleneck: Redis memory at scale
- 10M users × 500 posts in feed × 8 bytes = 40GB minimum
- Solution: Tiered storage — recent 100 posts in Redis, older posts in DynamoDB or Cassandra
New bottleneck: Feed diversity
- Pure chronological feed misses popular content from less-followed accounts
- Solution: Ranking service with ML model that scores posts by relevance, applied after fetching candidate set from Redis
Summary Table
| Issue | Root Cause | Solution | Impact |
|---|---|---|---|
| Slow feed queries | Expensive JOIN at read time | Fan-out on write (Redis sorted sets) | 100× faster reads |
| Database CPU 98% | Feed queries overwhelming DB | Offload to Redis | CPU drops to 25% |
| Celebrity write amplification | 10M Redis writes per post | Hybrid fan-out (on read for celebrities) | Bounded write cost |
| Feed diversity | Chronological only | ML ranking layer | Better user engagement |
| P99 latency 2.8s | Feed query + DB overload | Multi-layer optimization | P99 drops to 180ms |
Practice Problems
Your /search endpoint takes 3 seconds at P99. P50 is 200ms. Database CPU is at 40%. Application CPU is at 25%. What is likely the bottleneck and how do you fix it?
Solution
The P50/P99 gap suggests tail latency — likely caused by: 1) Cold cache misses (first request after TTL expires triggers expensive DB query), 2) Network latency spikes to downstream services, 3) GC pauses in JVM/Node.js. Fix: implement cache warming for hot queries, add connection pooling, add distributed tracing to pinpoint the slow hop, and profile for GC issues.A URL shortener handles 100M redirects/day. The read latency is 50ms P99. The database has 100M rows. Identify bottlenecks and propose solutions.
Solution
Bottleneck: every redirect hits the database. Solution: 1) CDN edge caching for popular URLs (top 10% of URLs handle 90% of traffic), 2) Redis cache layer between app and DB with high hit ratio, 3) Read replicas for DB reads, 4) Bloom filter at application layer to quickly reject non-existent URLs without hitting DB. Expected improvement: P99 from 50ms to < 5ms for cache hits.Quiz
1. According to Amdahl's Law, if 30% of a system is serial and 70% is parallelizable, what is the maximum speedup with infinite parallel resources?
2. Which of these is NOT a sign of a database connection pool bottleneck?
3. What is the 'cache stampede' problem?
4. What are the four golden signals of monitoring (Google SRE)?
5. When is 'fan-out on write' preferred over 'fan-out on read'?
6. What is the difference between load testing and stress testing?
Flashcards
Question
What is Amdahl's Law and why does it matter for bottleneck analysis?
Click to reveal answer
Answer
Amdahl's Law: Speedup = 1 / ((1-P) + P/S), where P is parallelizable fraction and S is speedup. It means optimizing a component that is 10% of latency gives at most 10% improvement. Always fix the largest bottleneck first.
Question
What are the four golden signals of monitoring?
Click to reveal answer
Answer
1) Latency — time to serve a request. 2) Traffic — requests per second. 3) Errors — rate of failed requests. 4) Saturation — how full the resource is (CPU, memory, connections).
Question
What is cache stampede and how do you prevent it?
Click to reveal answer
Answer
Cache stampede is when a popular cache entry expires and many requests simultaneously rebuild it, overwhelming the database. Prevention: use distributed locks (singleflight pattern), pre-warming, probabilistic early expiration, or stale-while-revalidate.
Question
What is fan-out on write vs fan-out on read?
Click to reveal answer
Answer
Fan-out on write: pre-compute and store results when data is written (O(1) reads, expensive writes). Fan-out on read: compute results on-demand at read time (O(1) writes, expensive reads). Trade-off depends on read/write ratio and follower count distribution.
Question
Name 3 common causes of high P99 latency with normal P50.
Click to reveal answer
Answer
1) GC pauses (JVM/Node.js stop-the-world collection). 2) Cache misses triggering expensive DB queries. 3) Network latency spikes to downstream services. 4) Tail latency from connection pool contention.
Question
How do you size a database connection pool?
Click to reveal answer
Answer
Rule of thumb: pool_size = (CPU cores × 2) + effective_spindle_count. Too few connections: requests wait. Too many: context switching overhead and memory waste. Monitor pool utilization and adjust. For PostgreSQL, total connections across all app servers must stay below max_connections.
Question
What is the 'celebrity problem' in fan-out architectures?
Click to reveal answer
Answer
When a user with millions of followers creates content, fan-out on write triggers millions of writes (one per follower). This causes massive write amplification. Solution: hybrid approach — fan-out on write for normal users, fan-out on read for celebrities with > threshold followers.
Revision Notes
Key Takeaways
- 1.A bottleneck limits total system throughput — find and fix the biggest one first
- 2.The four golden signals (Latency, Traffic, Errors, Saturation) cover most system health issues
- 3.P99 >> P50 gap indicates tail latency — investigate GC, cache misses, network spikes
- 4.Cache stampede is a real production issue — use singleflight or pre-warming
- 5.Fan-out on write trades read speed for write amplification — the celebrity problem is a real constraint
- 6.Connection pool sizing matters — too few causes waits, too many causes overhead
- 7.Load testing is not optional — soak tests catch issues load tests miss
Interview Tips
- •Always ask about metrics first: "What are the current latency percentiles and error rates?"
- •Use Amdahl's Law to prioritize: "The database query is 65% of latency, so optimizing it gives the most impact"
- •Mention specific tools: "I'd use pg_stat_statements to find slow queries and Jaeger for distributed tracing"
- •Discuss trade-offs: caching improves reads but adds staleness; fan-out on write improves reads but adds write amplification
- •For the social media feed, always mention the celebrity problem — it shows you've thought about edge cases
- •When proposing solutions, quantify the expected improvement: "Adding a Redis cache should reduce P99 from 200ms to 20ms"
Cheat Sheet
Bottlenecks Cheat Sheet
Identifying Bottlenecks
- Profiling: CPU flame graphs, memory profiles, I/O tracing, slow query logs
- Monitoring: Four golden signals (Latency, Traffic, Errors, Saturation)
- Load testing: k6, Locust, JMeter — simulate traffic to find breaking points
- Check: CPU, Memory, Disk I/O, Network, Database connections, Queue depth
Common Bottleneck Patterns
| Pattern | Symptoms | Root Cause |
|---|---|---|
| DB query slow | High DB CPU, slow queries | Missing indexes, N+1 queries, full scans |
| Network latency | High P99, normal P50 | Cross-region calls, DNS delays, no connection pooling |
| SPOF | Complete failure on component loss | No redundancy, single instance |
| Memory issues | High memory, GC pressure, OOM | Memory leaks, unbounded caches |
| Connection exhaustion | Timeouts, too many connections | Connection leaks, undersized pool |
Amdahl's Law
- Speedup = 1 / ((1-P) + P/S)
- Serial portion limits maximum speedup
- Always optimize the largest bottleneck first
- Optimizing 10% of latency → at most 10% improvement
Resolution Patterns
- Caching: Cache-aside, write-through, write-behind. Layers: in-process → Redis → CDN
- Read replicas: Scale reads, introduces replication lag
- Connection pooling: PgBouncer, HikariCP. Size = (cores × 2) + spindles
- Async processing: SQS/Kafka for non-critical work (emails, analytics, images)
- Denormalization: Pre-compute JOINs for read-heavy systems
- Fan-out on write: Pre-compute feeds/timelines at write time
Performance Testing
- Load test: Expected peak (validates capacity)
- Stress test: Beyond peak (finds breaking point)
- Soak test: Sustained hours (finds memory leaks, connection exhaustion)
- Spike test: Sudden burst (tests auto-scaling)
Social Media Feed Bottleneck Walkthrough
- Feed query JOIN follows + posts → 800ms (65% of latency)
- Fan-out on write: push post_id to followers' Redis sorted sets
- Feed read: ZREVRANGE (O(log N) → 8ms)
- Celebrity problem: hybrid fan-out (write for normal, read for celebrities)
- Result: P99 from 2.8s to 180ms