Before measuring — Your intuition about bottlenecks is wrong 90% of the time. Profile first.
Fundamentals
The mental models you need before optimizing anything
1 Why Performance Matters
Performance is not a feature — it is the product. Every millisecond of delay compounds into lost revenue, frustrated users, and wasted infrastructure spend. But knowing when to optimize is just as important as knowing how.
The Business Case for Speed
User Experience
Studies from Google and Amazon consistently show that every 100ms of added latency costs roughly 1% of revenue. Users perceive pages loading in under 100ms as instantaneous. At 1 second, they notice. At 3 seconds, 53% of mobile users abandon the page entirely.
- 0–100ms — feels instant
- 100–300ms — slight perceptible delay
- 300–1000ms — noticeable, focus shifts
- 1s+ — user mentally context-switches
- 10s+ — user leaves
Cost Reduction
Twice-as-fast code requires half the servers. A 50% reduction in CPU usage at scale can translate to millions of dollars per year in cloud bills. Instagram famously saved hundreds of servers by optimizing a single Python function.
Scalability Headroom
Efficient systems handle traffic spikes without auto-scaling panic. A service using 20% of its CPU at baseline can absorb a 4x spike. One running at 80% topples on a 1.5x spike.
Competitive Advantage
Google found that a 500ms delay in search results caused a 20% drop in traffic. Shopify reports that stores in the fastest quartile convert 2.4x more than the slowest. Speed is a moat.
"Premature optimization is the root of all evil." — Donald Knuth (often misquoted out of context)
When NOT to Optimize
Knuth's full quote continues: "Yet we should not pass up our opportunities in that critical 3%." The real message: don't optimize without data. Here's when to hold off:
Before correctness — A fast wrong answer is worthless. Get it right, then get it fast.
Before product-market fit — Optimizing a feature nobody uses is pure waste. Validate first.
When the code is already fast enough — If your API responds in 2ms and the SLA is 200ms, move on to something that matters.
2 Latency vs Throughput
Latency is how long one operation takes. Throughput is how many operations complete per unit of time. They are related but not inverses — optimizing one can hurt the other.
The Bathtub Analogy
Imagine filling a bathtub. Latency is how long until the first drop reaches the drain. Throughput is how many gallons per minute flow through. A wide pipe (high throughput) doesn't mean the water arrives sooner (low latency). Conversely, a narrow but short pipe may have excellent latency but poor throughput.
In systems: batching increases throughput at the cost of latency. Pipelining can improve both. Overloading destroys both.
Percentiles: Why Averages Lie
Average latency hides your worst users. If 99 requests take 10ms and 1 takes 5 seconds, the average is ~60ms. Looks fine. But that 1 request represents a real user having a terrible experience — and it's probably your most important user (heavy users make more requests and are more likely to hit tail latency).
| Percentile | Meaning | Why It Matters |
|---|---|---|
| P50 | Median — half of requests are faster | Typical user experience |
| P90 | 90th percentile | 1 in 10 requests is slower than this |
| P95 | 95th percentile | Common SLA target |
| P99 | 99th percentile | Tail latency — often 5-50x the median |
| P99.9 | 99.9th percentile | 1 in 1000 — matters at scale (1M req = 1000 affected) |
Coordinated Omission
A subtle but devastating measurement bug. Most load testing tools measure latency from when the request was sent, not from when it should have been sent. If the server stalls for 5 seconds and the client is waiting, the stall only shows up in one request's latency — but it delayed all the requests that were supposed to fire during that 5 seconds. Gil Tene's wrk2 and HdrHistogram are designed to combat this.
3 Latency Numbers Every Programmer Should Know
Jeff Dean's famous list, updated for modern hardware (~2024-2025 era). Memorize the orders of magnitude — the exact numbers change yearly, but the ratios stay remarkably stable.
Visual Scale (logarithmic, 1 ns = 1 second analogy)
Architectural Implications
Memory >> Disk
RAM is ~1,000x faster than SSD for random reads. This is why caches exist and why Redis obliterates database queries for hot data. If it fits in RAM, keep it there.
Sequential >> Random
Sequential disk reads are 100x faster than random on HDD, ~6x on SSD. This drives the design of LSM trees (LevelDB, RocksDB, Cassandra) and WAL-based systems.
Network is Slow
A datacenter round-trip is 500 us — that's 5,000x a RAM access. Every microservice call is expensive. Batch calls. Reduce chattiness. Co-locate hot paths.
Compression is (Nearly) Free
Compressing 1KB takes 3 us. Sending 1KB over the network (at 1Gbps) takes ~8 us. If compression saves even 30% of bytes, it's a net win over the network.
4 Amdahl's Law & Scalability
Amdahl's Law defines the theoretical maximum speedup from parallelizing a computation. It is the single most important formula in performance engineering.
The Formula
Speedup = 1 / ( S + (1 - S) / N )
Where S = fraction of work that is serial (cannot be parallelized), and N = number of parallel processors.
If 5% of your work is serial (S = 0.05), the maximum speedup with infinite processors is 1/0.05 = 20x. No matter how many cores you throw at it.
Universal Scalability Law (USL)
Neil Gunther's USL extends Amdahl's Law by adding a coherence penalty (the cost of keeping shared state consistent across processors). While Amdahl's Law says throughput plateaus, USL predicts that throughput can actually decrease past a certain point — a phenomenon called retrograde scalability.
C(N) = N / (1 + alpha(N-1) + beta * N * (N-1))
- alpha = contention parameter (serial fraction, like Amdahl's S)
- beta = coherence parameter (cost of cross-node coordination)
When beta > 0, adding more nodes eventually makes the system slower. This is why distributed databases don't scale linearly — coordination (consensus, cache invalidation, lock management) has a quadratic cost.
Practical Implications
- Find the serial bottleneck first. If 20% of your pipeline is serial, max speedup is 5x regardless of hardware.
- Measure before adding cores. Going from 4 to 8 cores gives at most 2x. Going from 64 to 128 gives almost nothing.
- Reduce coordination. Shared locks, distributed consensus, and cache coherence are the enemies of scalability.
- Partition, don't share. Independent shards scale linearly. Shared-everything architectures hit USL's coherence wall.
5 Little's Law
The most useful formula in capacity planning, and one of the few results in queueing theory that requires almost no assumptions.
The Formula
L = λ × W
- L = average number of items in the system (concurrent requests, items in queue)
- λ (lambda) = average arrival rate (requests per second)
- W = average time each item spends in the system (latency)
Practical Examples
Web Server Sizing
Your service handles 1,000 req/s with an average latency of 50ms.
L = 1000 × 0.05 = 50 concurrent requests
You need at least 50 worker threads to handle this without queueing. With some headroom for variance, size to ~75-100 threads.
Thread Pool Sizing
Each database query takes 10ms. You need to handle 500 queries/s.
L = 500 × 0.01 = 5 concurrent connections
A connection pool of 10 handles this easily with room for bursts. HikariCP's default of 10 connections is not an accident.
Queue Depth
Messages arrive at 200/s. Processing takes 100ms each.
L = 200 × 0.1 = 20 messages in-flight
If your consumer only has 10 workers, a queue will build. At 20 workers, the system is stable. Below 20, latency grows without bound.
The Queueing Trap
When utilization approaches 100%, queueing delay explodes. For an M/M/1 queue, the average wait time is:
W_queue = W_service / (1 - utilization)
At 50% utilization, wait = 2x service time. At 90%, wait = 10x. At 99%, wait = 100x. This is why you should never run a system above ~70-80% utilization — the latency penalty is not linear.
Measuring
You can't optimize what you can't measure
6 Profiling
Profiling is the act of measuring where your program spends its time and memory. Without profiling, optimization is guesswork. With profiling, you find the 5% of code that accounts for 95% of the cost.
CPU Profiling Methods
Sampling Profiler
Periodically interrupts the program (e.g., every 10ms) and records the current stack trace. Builds a statistical picture of where time is spent.
- Low overhead (1-5%)
- Safe for production
- Misses short functions
- Best for: finding hot methods
Instrumentation Profiler
Injects code at every function entry/exit to record exact timing and call counts. Gives precise measurements.
- High overhead (10-100x slower)
- Not production-safe
- Exact counts & times
- Best for: deep analysis offline
Wall-Clock vs CPU Time
Wall-clock time = real elapsed time, including time spent waiting (I/O, locks, sleeping).
CPU time = time actually executing on the CPU.
If a function takes 100ms wall-clock but only 2ms CPU, it's I/O-bound. Optimizing the algorithm won't help — optimize the I/O.
Profiling Tools
| Tool | Language | Type | Production-Safe | Notes |
|---|---|---|---|---|
| async-profiler | Java | Sampling | Yes | Gold standard for JVM. Handles native frames. |
| VisualVM | Java | Both | Caution | Good GUI, attach to running process |
| perf | C/C++/System | Sampling | Yes | Linux kernel profiler. Hardware counters. |
| pprof | Go | Sampling | Yes | Built into Go runtime. CPU, memory, goroutine. |
| Chrome DevTools | JavaScript | Sampling | Caution | CPU, memory, network waterfall |
| py-spy | Python | Sampling | Yes | No code changes needed. Attaches to running process. |
| rbspy | Ruby | Sampling | Yes | Similar to py-spy for Ruby |
| dotTrace | .NET | Both | Caution | JetBrains. Excellent timeline view. |
Memory Profiling
- Heap dumps — snapshot of every object on the heap. Find what's using memory. Tools: jmap (Java), pprof (Go), memray (Python).
- Allocation profiling — tracks where objects are allocated. More useful than heap dumps for GC pressure. async-profiler's alloc mode is excellent.
- Leak detection — compare heap dumps over time. Growing object counts = leak. In Go, pprof's
inuse_objectsvsalloc_objectstells the story.
7 Flame Graphs
Invented by Brendan Gregg, flame graphs are the single most effective visualization for understanding CPU profiles. Once you learn to read them, you'll never go back to flat profiles.
How to Read a Flame Graph
- X-axis = population (percentage of samples), NOT time. Wider = more CPU time spent in that function.
- Y-axis = stack depth. Bottom = entry point (main), top = leaf function where CPU was actually executing.
- Color = random warm palette (no meaning by default), or can encode type (Java = green, native = yellow, kernel = red).
- Wide plateau at top = hot function consuming lots of CPU. This is your optimization target.
- Wide tower = deep call chain consuming lots of CPU. Look for the widest frame — that's the bottleneck.
Flame vs Icicle
Flame Graph (bottom-up)
Root at bottom, leaves at top. The "fire" rises. Traditional format. Best for finding hot leaf functions.
Icicle Graph (top-down)
Root at top, leaves at bottom. "Stalactites." Better for understanding call hierarchy. SpeedScope uses this by default.
Differential Flame Graphs
Compare two profiles (before/after optimization, or two versions). Functions that got slower appear in red; functions that got faster appear in blue. Generate with Brendan Gregg's difffolded.pl or use SpeedScope's compare mode.
Generating Flame Graphs
- Java:
async-profiler -d 30 -f profile.html <pid>— produces an interactive HTML flame graph directly. - Go:
go tool pprof -http=:8080 cpu.prof— built-in flame graph view in the web UI. - Linux (any language):
perf record -g -F 99 -p <pid> -- sleep 30, thenperf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg - Node.js:
node --prof app.js, then process with 0x or clinic flame.
8 Benchmarking
Benchmarking is deceptively hard. The difference between a meaningful benchmark and a misleading one is enormous. Most benchmarks in blog posts are wrong.
Micro vs Macro Benchmarks
Microbenchmarks
Measure a single function or operation in isolation. Nanosecond-level.
- Use: comparing two sort implementations
- Danger: results may not transfer to real workloads
- Tools: JMH (Java), criterion (Rust), BenchmarkDotNet (.NET)
Macrobenchmarks
Measure the entire system under realistic conditions. Millisecond to second level.
- Use: validating end-to-end performance
- More realistic but harder to isolate cause
- Tools: wrk, k6, hey, custom test harnesses
Common Pitfalls
Dead Code Elimination
Compilers are smart. If the result of your benchmark isn't used, the compiler may optimize away the entire computation. JMH uses Blackhole.consume() to prevent this. In C, use volatile or asm volatile("" ::: "memory").
Constant Folding
If inputs are compile-time constants, the compiler may compute the result at compile time. Always use runtime-generated or opaque inputs.
JIT Warm-up
JVM code starts interpreted, then gets JIT-compiled after ~10,000 invocations. Benchmarking before warmup measures the interpreter, not your code. JMH handles warmup iterations automatically.
Ignoring Variance
A single benchmark run is meaningless. Run multiple iterations, report mean ± standard deviation, check for outliers. If the coefficient of variation exceeds 5%, something is unstable.
HTTP Benchmarking Tools
| Tool | Language | Strengths | Use When |
|---|---|---|---|
| wrk | C | Very fast, Lua scripting | Max throughput testing |
| wrk2 | C | Corrects coordinated omission | Accurate latency measurement |
| k6 | Go | JavaScript test scripts, scenarios | Complex load patterns |
| hey | Go | Simple, fast | Quick smoke tests |
| ab | C | Ubiquitous (Apache Bench) | Quick checks (use wrk instead) |
How to Report Results
A good benchmark report includes: P50, P95, P99 latencies, throughput (req/s), error rate, resource utilization (CPU, memory), test duration, hardware specs, software versions, and reproduction instructions. Without these, the numbers are useless.
9 Load Testing
Load testing validates that your system meets its performance requirements under realistic conditions. Different types of load tests answer different questions.
Types of Load Tests
| Type | Purpose | Pattern | Duration |
|---|---|---|---|
| Smoke | Verify system works under minimal load | 1-5 users | 1-5 min |
| Load | Validate expected traffic levels | Normal peak load | 15-60 min |
| Stress | Find the breaking point | Ramp to 2-3x peak | 15-30 min |
| Spike | Test sudden traffic bursts | Instant jump to 10x | 5-10 min |
| Soak | Find memory leaks, resource exhaustion | Normal load, extended | 4-24 hours |
Load Testing Tools
k6 Recommended
Modern, developer-friendly. Write tests in JavaScript. Built-in scenarios, thresholds, and metrics. Free and open source. Best docs in the category.
Gatling
Scala-based. Excellent HTML reports. Good for CI integration. Best for JVM shops. Free version is powerful.
Locust
Python-based. Write tests as Python classes. Distributed load generation. Best for Python teams.
JMeter
GUI-based. Huge feature set. Steep learning curve. XML test plans are hard to version control. Legacy but still widely used.
Designing Realistic Load Profiles
- Analyze real traffic — use access logs to understand request distribution, think time, session patterns
- Use varied data — hitting the same URL with the same parameters tests the cache, not your system
- Model think time — real users pause between clicks; remove think time and you're testing something unrealistic
- Include all request types — a mix of reads, writes, uploads, searches at realistic ratios
- Ramp up gradually — don't jump to full load; ramp over 2-5 minutes to let connection pools warm up
10 Monitoring & Alerting
Monitoring is how you know your system is healthy in production. Good monitoring prevents outages. Great monitoring tells you why the outage happened in under 60 seconds.
The RED Method (for Services)
For every service, monitor these three things. Rate tells you if traffic changed. Errors tell you if something broke. Duration tells you if something slowed down. Together they cover most failure modes.
The USE Method (for Resources)
For every resource (CPU, disk, network, memory), check: How busy is it? Is work queuing? Are there errors? Brendan Gregg designed this to systematically find bottlenecks.
SLOs, SLIs, and Error Budgets
| Concept | Definition | Example |
|---|---|---|
| SLI (Indicator) | A measurement of service health | P99 latency of the /api/search endpoint |
| SLO (Objective) | A target value for an SLI | P99 latency < 200ms for 99.9% of requests |
| SLA (Agreement) | A contract with consequences | If P99 > 200ms for >0.1% of requests, customer gets credit |
| Error Budget | Allowed failures before SLO breach | 99.9% SLO = 43.8 min of downtime per month |
Prometheus + Grafana Stack
This is the de facto open-source monitoring stack. Prometheus pulls metrics via HTTP (pull model), stores them as time series, and supports powerful queries. Grafana visualizes them. Alertmanager handles routing and deduplication of alerts.
Golden Signals (Google SRE)
Google's four golden signals are similar to RED but add Saturation: how "full" a service is. A system at 95% memory is one bad deploy from OOM-killing. Monitor: Latency, Traffic, Errors, Saturation.
CPU & Compute
Understanding the metal your code runs on
11 CPU Performance
Modern CPUs are marvels of prediction and speculation. Understanding how they work (and fail) is essential for writing truly fast code.
The Instruction Pipeline
Branch Prediction
CPUs predict which branch an if/else will take and start executing speculatively. Modern predictors are >95% accurate on typical code. But when they're wrong, the penalty is 10-20 cycles (the pipeline must be flushed and restarted).
The Sorted Array Example
A famous Stack Overflow question showed that processing a sorted array was 6x faster than an unsorted one — same algorithm, same data. Why? The branch predictor could learn the pattern in sorted data (long runs of taken/not-taken), but random data defeated it. Branchless alternatives (using bitwise ops or conditional moves) eliminated the penalty entirely.
Cache Lines
CPUs don't read individual bytes from memory — they read 64-byte cache lines. When you access one byte, the adjacent 63 bytes come along for free. This has profound implications:
- Spatial locality wins — accessing sequential memory is fast because it's already in cache
- Struct padding matters — a struct that spans two cache lines requires two memory accesses
- Array of structs vs struct of arrays — if you only access one field, SoA keeps that field's data contiguous in cache
False Sharing
When two threads write to different variables that happen to be on the same cache line, the cache coherence protocol bounces the line between cores — even though there's no actual data sharing. This can cause 10-100x slowdown vs the expected performance.
Fix: Pad variables to 64-byte boundaries. In Java: @Contended annotation. In C/C++: alignas(64).
NUMA (Non-Uniform Memory Access)
Multi-socket servers have memory attached to each socket. Accessing local memory takes ~100ns. Accessing remote memory (another socket) takes ~150-300ns. For performance-critical applications on multi-socket machines, pin threads to cores and allocate memory on the local NUMA node.
Cache Hierarchy
12 Algorithmic Complexity
Big O notation is the first tool for performance analysis. But real-world performance is messier than theory suggests — constant factors, cache effects, and input distributions all matter.
Big O Refresher (Practical Focus)
| Complexity | Name | N=10 | N=1,000 | N=1,000,000 | Real-World Example |
|---|---|---|---|---|---|
| O(1) | Constant | 1 | 1 | 1 | HashMap lookup |
| O(log n) | Logarithmic | 3 | 10 | 20 | Binary search, B-tree lookup |
| O(n) | Linear | 10 | 1,000 | 1,000,000 | Array scan, single loop |
| O(n log n) | Linearithmic | 33 | 10,000 | 20,000,000 | Sorting (mergesort, quicksort) |
| O(n²) | Quadratic | 100 | 1,000,000 | 10¹² | Nested loops, naive string matching |
| O(2ⁿ) | Exponential | 1,024 | ∞ | ∞ | Brute-force subsets, naive Fibonacci |
When O(n²) is Actually Fine
If N is always small (say, <100), an O(n²) algorithm with a tiny constant factor often beats an O(n log n) algorithm with a larger constant. Insertion sort beats quicksort for N<16 — that's why most sort libraries use hybrid sorting (Timsort, IntroSort) that switches to insertion sort for small partitions.
Don't blindly chase asymptotic complexity. Profile with real data sizes. A O(n) algorithm that thrashes the cache may lose to a cache-friendly O(n log n) algorithm.
Real-World Examples Where Algorithm Choice Made 100x Difference
String Matching
Naive O(nm) substring search vs Aho-Corasick O(n + m + z) for matching multiple patterns. A malware scanner searching for 10,000 patterns went from 45 minutes to 12 seconds — a 225x speedup.
Graph Shortest Path
Dijkstra with a binary heap O((V+E) log V) vs BFS for unweighted graphs O(V+E). A social network "degrees of separation" feature went from 8s to 80ms by recognizing edges were unweighted.
Deduplication
Nested loop dedup O(n²) vs HashSet dedup O(n). Processing 10 million records went from 14 hours to 3 seconds.
Amortized Complexity
Some operations are expensive occasionally but cheap on average. ArrayList.add() is O(1) amortized — most adds are instant, but every ~Nth add triggers an O(n) resize. The total cost over N operations is still O(n), so each one is O(1) amortized. This is fine for throughput but can cause latency spikes — which matters in real-time systems.
13 Concurrency Performance
Concurrency is where hardware parallelism meets software coordination. Getting it right yields linear speedups. Getting it wrong yields bugs and makes things slower.
Lock Contention
A lock serializes access to shared state. Under contention (many threads competing for the same lock), the lock becomes a bottleneck — effectively making your parallel program serial. Strategies to reduce contention:
- Reduce critical section size — hold the lock for the minimum time necessary
- Use lock striping — ConcurrentHashMap uses 16 segments instead of one lock
- Use read-write locks — allow concurrent readers, only serialize writers
- Eliminate shared state — thread-local storage, sharding, immutable data
Lock-Free Data Structures
Lock-free structures use atomic operations (CAS — Compare And Swap) instead of locks. They guarantee that some thread always makes progress, even if others are delayed. Examples: ConcurrentLinkedQueue, AtomicLong, Disruptor ring buffer.
Caution: Lock-free code is extremely hard to write correctly. Use well-tested libraries (java.util.concurrent, crossbeam for Rust) rather than rolling your own.
Thread Pool Sizing
CPU-Bound Work
Threads = N cores (or N+1)
More threads than cores means context switching without benefit. Each context switch costs 1-10 us and pollutes the cache.
I/O-Bound Work
Threads = N × (1 + wait_time / compute_time)
If a thread spends 90% of its time waiting on I/O, you need 10x the cores in threads. E.g., 8 cores, 90% I/O: 8 × (1 + 9) = 80 threads.
Context Switching Cost
Every thread context switch costs 1-10 microseconds of direct overhead, plus indirect costs from cache pollution (L1/L2 caches go cold). With 10,000 threads, the scheduler alone can consume 100% of a core. This is why event-loop architectures (Node.js, Netty, Nginx) outperform thread-per-request models at high concurrency.
Memory
The hidden variable that dominates modern performance
14 Memory Performance
On modern hardware, memory access patterns often matter more than algorithmic complexity. A cache-friendly O(n log n) sort beats a cache-hostile O(n) scan. Understanding the memory hierarchy is the key to unlocking hardware performance.
Cache Hierarchy Latency
| Level | Size (typical) | Latency | Bandwidth | Analogy |
|---|---|---|---|---|
| L1 Cache | 32-64 KB per core | ~1 ns (4 cycles) | ~1 TB/s | Your desk |
| L2 Cache | 256 KB - 1 MB per core | ~4 ns (12 cycles) | ~500 GB/s | Filing cabinet |
| L3 Cache | 8 - 64 MB shared | ~12 ns (40 cycles) | ~200 GB/s | Office supply room |
| DRAM | 16 - 512 GB | ~100 ns (300 cycles) | ~50 GB/s | Warehouse downtown |
Spatial & Temporal Locality
Spatial Locality
Access nearby memory addresses. Arrays win here — iterating through an array loads one cache line and gets 16 ints for free. Linked lists load one cache line per node (pointer-chasing), often with 0 useful adjacent data.
Temporal Locality
Access the same data again soon. Hot loops over small data sets keep everything in L1. This is why tiling/blocking matrix multiplication achieves near-peak FLOPS — it processes small blocks that fit in cache.
Data-Oriented Design
Traditional OOP organizes data around objects. Data-oriented design organizes data around access patterns — keeping the data you process together actually together in memory.
Struct of Arrays vs Array of Structs
Array of Structs (AoS)
struct Particle {
x, y, z: f32,
vx, vy, vz: f32,
mass: f32,
color: u32,
}
particles: [Particle; N]
Cache loads all fields even if you only need (x,y,z).
Struct of Arrays (SoA)
struct Particles {
x: [f32; N],
y: [f32; N],
z: [f32; N],
vx: [f32; N],
...
}
Only loads the fields you touch. SIMD-friendly. 2-4x faster for hot loops.
15 Memory Management
Allocation and deallocation are among the most frequent operations in any program. How they're managed — manually, by GC, or via pools — can make or break performance.
Garbage Collection Tuning
Java GC (Hotspot)
- G1GC — default since Java 9. Good balance. Tune with
-XX:MaxGCPauseMillis - ZGC — sub-millisecond pauses, even with TB heaps. Use for latency-sensitive workloads.
- Shenandoah — similar to ZGC, available in OpenJDK
- Key tuning: heap size (too small = frequent GC, too large = long GC), new gen ratio, survivor spaces
Go GC
- Concurrent, tri-color mark-and-sweep
- Tune via
GOGC(default 100 = GC when heap doubles) - Go 1.19+ has
GOMEMLIMIT— set max memory and let GC optimize - Low-latency design: sub-ms pauses, but can use 2x memory
Object Pooling
For frequently allocated/deallocated objects (buffers, connections, message objects), pooling eliminates GC pressure. Java: use ThreadLocal pools or libraries like Netty's pooled byte buffers. Go: sync.Pool. Key rule: only pool objects that are expensive to create or created at extremely high rates.
Memory Leaks: Common Patterns
Detection Tools
Java: Eclipse MAT for heap dump analysis, jmap -histo for quick overview, JFR for allocation tracking. Go: pprof heap profiles. Python: tracemalloc, objgraph. General: compare heap snapshots over time — anything growing linearly is a leak candidate.
16 Data Structures & Performance
Choosing the right data structure is the highest-leverage optimization decision. The best algorithm with the wrong data structure is like a Formula 1 engine in a shopping cart.
ArrayList vs LinkedList: The Cache Locality Victory
In theory, LinkedList has O(1) insert/delete at a known position vs ArrayList's O(n). In practice, ArrayList is faster at almost everything because of cache locality. Iterating an ArrayList accesses sequential memory (prefetcher-friendly). Iterating a LinkedList follows scattered pointers (cache-hostile).
Benchmarks consistently show ArrayList is 5-10x faster for iteration, and even faster for random access. LinkedList only wins when you have a stable iterator and insert/remove at that iterator's position — a rare use case.
Rule of thumb: Default to ArrayList/Vec/slice. Use LinkedList only if you can prove you need O(1) splice operations.
HashMap Internals
- Load factor — Java's default is 0.75 (resize when 75% full). Lower = faster lookups, more memory. Higher = slower lookups, less memory.
- Hash function quality — poor hash functions create clusters, degrading O(1) to O(n). Java 8+ uses a tree structure for long chains.
- Initial capacity — if you know you'll have 1000 entries, create with capacity 1334 (1000/0.75) to avoid resizing.
Choosing the Right Structure
| Access Pattern | Best Structure | Why |
|---|---|---|
| Key-value lookup | HashMap / dict | O(1) average lookup |
| Sorted key-value | B-tree / TreeMap | O(log n) lookup + range queries |
| Membership test | HashSet | O(1) contains |
| Probabilistic membership | Bloom filter | O(k) with ~1% false positives, zero false negatives, tiny memory |
| Priority queue | Binary heap | O(log n) insert/extract-min |
| FIFO queue | ArrayDeque | O(1) amortized, cache-friendly (not LinkedList!) |
| Append-heavy list | ArrayList / Vec | O(1) amortized append, cache-friendly iteration |
| Concurrent key-value | ConcurrentHashMap | Lock striping, high concurrency |
| Range queries on disk | B+ tree (DB index) | Minimal disk seeks, high fan-out |
| Write-heavy on disk | LSM tree | Sequential writes, compaction |
Bloom Filters
A probabilistic data structure that answers "is this element in the set?" with either "definitely no" or "probably yes." Uses a bit array and multiple hash functions. At 1% false positive rate, it uses only ~10 bits per element (regardless of element size).
Use cases: avoiding expensive disk/network lookups when the item doesn't exist (CDNs, databases, spell checkers, web crawlers checking "already visited").
I/O & Network
Where most real-world applications actually spend their time
17 Disk I/O
Despite SSDs making random I/O much faster than spinning disks, the gap between memory and disk remains enormous. Understanding disk I/O patterns is critical for any data-intensive application.
Sequential vs Random I/O
| Operation | HDD | SSD (SATA) | SSD (NVMe) | RAM |
|---|---|---|---|---|
| Sequential read (MB/s) | 100-200 | 500-550 | 3,000-7,000 | 30,000+ |
| Random read (4K IOPS) | 100-200 | 75,000-100,000 | 500,000-1,000,000 | millions |
| Random read latency | 5-10 ms | 50-100 us | 10-20 us | ~100 ns |
| Sequential vs random ratio | ~1000x | ~4-6x | ~2-3x | ~1x |
Key I/O Concepts
Page Cache
The OS caches recently read disk data in RAM. Second reads of the same data are served from memory. This is why Kafka performs so well — it relies on the OS page cache instead of managing its own cache, letting the OS do what it does best.
Buffering & Batching
Writing one byte at a time means one syscall per byte — catastrophic. BufferedOutputStream (Java), bufio.Writer (Go), and fwrite (C) batch small writes into large ones. Always buffer I/O.
mmap
Memory-mapped files let you access files as if they were in memory. The OS handles paging transparently. Excellent for read-heavy workloads on files that fit in RAM. Used by SQLite, LMDB, and many database engines.
Direct I/O
Bypasses the page cache. Used when you manage your own cache (databases like MySQL InnoDB). Avoids double-caching but loses the OS's optimizations.
io_uring (Linux 5.1+)
A modern async I/O interface for Linux that eliminates syscall overhead. Uses shared ring buffers between user and kernel space. Supports batching multiple I/O operations into a single submission. Benchmarks show 2-3x throughput improvement over epoll for I/O-heavy workloads. Libraries: liburing (C), io-uring crate (Rust), tokio-uring.
Write-Ahead Log (WAL) Pattern
Sequential writes are fast. Random writes are slow. WAL converts random writes into sequential appends, giving both durability and speed. Used by: PostgreSQL, SQLite, LevelDB, Kafka, etcd, every serious database.
18 Network Performance
Network is usually the slowest link in a distributed system. A datacenter round-trip is 500 us — 5,000x a memory access. Every network call you can eliminate is a massive win.
Bandwidth vs Latency
Bandwidth = how much data you can send per second (throughput). Latency = how long until the first bit arrives. Bandwidth has improved ~1000x in 20 years (10Mbps to 10Gbps). Latency has barely improved — it's bounded by physics (speed of light). For small messages, latency dominates. For large transfers, bandwidth dominates.
TCP Tuning
TCP_NODELAY
Disables Nagle's algorithm, which buffers small packets to reduce overhead. For request-response protocols (HTTP, RPC), always enable TCP_NODELAY. Nagle can add up to 200ms delay waiting for the buffer to fill.
Window Size
TCP's receive window controls how much data can be in-flight before an ACK. For high-bandwidth, high-latency links (cross-continent), increase the window. BDP = bandwidth × delay. A 1Gbps link with 100ms RTT needs a 12.5 MB window.
Connection Pooling
TCP handshake costs 1.5 RTTs. TLS adds 1-2 more. A connection pool reuses established connections, amortizing this cost. Every HTTP client and database driver should use connection pooling.
Keep-Alive
HTTP/1.1 keeps connections open for subsequent requests (instead of close-after-response). HTTP/2 multiplexes many requests over a single connection. Both eliminate repeated TCP/TLS handshakes.
HTTP/2 Multiplexing
Compression Trade-offs
| Algorithm | Compression Ratio | Compress Speed | Decompress Speed | Best For |
|---|---|---|---|---|
| LZ4 | 2.0-2.5x | ~780 MB/s | ~4,000 MB/s | Speed-critical (logs, IPC) |
| Snappy | 1.5-2.0x | ~500 MB/s | ~1,500 MB/s | Speed-critical (Bigtable) |
| Zstd | 2.5-3.5x | ~400 MB/s | ~1,200 MB/s | Best all-rounder |
| Gzip | 3.0-4.0x | ~50 MB/s | ~400 MB/s | HTTP (universal support) |
| Brotli | 3.5-5.0x | ~20 MB/s | ~400 MB/s | Static web assets |
19 Database Performance
The database is the most common bottleneck in web applications. A single missing index can turn a 2ms query into a 20-second table scan. Database optimization has the highest ROI of any performance work.
Query Optimization
EXPLAIN ANALYZE is Your Best Friend
Before optimizing, understand what the database is doing. EXPLAIN ANALYZE (PostgreSQL) or EXPLAIN FORMAT=JSON (MySQL) shows the actual execution plan, row estimates, and time spent in each step.
Key things to look for: Seq Scan on large tables (missing index), Nested Loop with high row count (N+1 at the DB level), Sort without index (disk sort), Hash Join with memory spill.
Indexing Strategy
B-tree Index (Default)
Equality + range queries. The workhorse. Leftmost prefix rule: an index on (a, b, c) supports queries on (a), (a, b), and (a, b, c) but NOT (b) or (c) alone.
Covering Index
Includes all columns the query needs, so the DB never touches the table. CREATE INDEX idx ON orders(customer_id) INCLUDE (total, status). Can turn a 10ms query into a 0.1ms query.
Partial Index
Only indexes rows matching a condition. CREATE INDEX idx ON orders(created_at) WHERE status = 'pending'. Smaller index = faster lookups + less storage.
The N+1 Problem
Connection Pooling
Each database connection costs ~5-10 MB of memory on the server. Creating a connection takes 5-50ms (TCP + auth). Connection pools (HikariCP for Java, PgBouncer for PostgreSQL, pgx pool for Go) reuse connections. HikariCP's default pool size of 10 handles more load than most people expect — see the HikariCP wiki's "About Pool Sizing" page, which argues that fewer connections with less contention outperform many connections competing for CPU/disk.
Other Database Optimizations
- Read replicas — route read queries to replicas to reduce primary load. Watch for replication lag on writes-then-reads.
- Query caching — cache frequent query results in Redis/Memcached. Invalidate on writes. Massive win for read-heavy workloads.
- Denormalization — store pre-computed data to avoid expensive JOINs. Trade write complexity for read speed. Common in analytics and reporting.
- Batch operations — INSERT 1000 rows in one statement instead of 1000 individual INSERTs. 10-100x faster.
Application Level
Patterns and techniques at the code and framework level
20 Caching Strategies
"There are only two hard things in computer science: cache invalidation and naming things." — Phil Karlton. Caching is the single most impactful performance technique, and the single hardest to get right.
Where to Cache
Each layer reduces load on the layer behind it. Browser caching eliminates network requests entirely. CDN caching serves content from edge nodes close to users. App-level caching (Redis/Memcached) avoids expensive computations and database queries.
Cache Patterns
Cache-Aside (Lazy Loading)
App checks cache first. On miss, queries the database, then stores in cache. Simple, widely used. Risk: stale data if DB is updated without cache invalidation.
Write-Through
Writes go to cache AND database simultaneously. Cache is always consistent. Higher write latency. Good when reads are frequent and consistency is critical.
Write-Behind (Write-Back)
Writes go to cache; database is updated asynchronously. Lowest write latency. Risk: data loss if cache crashes before flush. Used in hardware disk caches and some distributed systems.
Cache Invalidation Strategies
- TTL (Time To Live) — simplest. Data expires after N seconds. Good enough for most cases. Tune TTL based on data freshness requirements.
- Event-based invalidation — on data change, explicitly delete or update cache entries. More complex but ensures consistency.
- Version keys — include a version number in the cache key. Increment on change. Old entries expire naturally via TTL.
Cache Stampede (Thundering Herd)
When a popular cache entry expires, hundreds of concurrent requests all miss the cache simultaneously and all hit the database. Solutions:
- Lock/single-flight — only one request refreshes; others wait for the result
- Stale-while-revalidate — serve stale data immediately while refreshing in background
- Probabilistic early expiration — randomly refresh before TTL with increasing probability as expiration approaches
Hot Key Problem
A single cache key receiving millions of requests per second can overwhelm a single Redis shard. Solutions: replicate the key across multiple shards with randomized suffixes (key:1, key:2, ..., key:N), use local in-process caching for the hottest keys, or use a dedicated "hot key" cache tier.
21 Async & Non-Blocking
Async programming lets a single thread handle thousands of concurrent operations by not blocking on I/O. It's the foundation of every high-performance server.
Event Loops
Virtual Threads (Java 21+)
Java's virtual threads (Project Loom) give you the programming model of blocking code with the scalability of async. Create millions of virtual threads — the JVM multiplexes them onto a small pool of OS threads. Write Thread.sleep() or blocking I/O as usual; the runtime handles the rest.
This largely eliminates the need for reactive frameworks (WebFlux, RxJava) in many Java applications. Write simple blocking code that scales.
When Async Does NOT Help
CPU-bound work — if your thread is computing (not waiting), async adds overhead without benefit. For matrix multiplication, video encoding, or cryptography, use thread pools with one thread per core, not async.
Backpressure
When a producer generates data faster than a consumer can process it, backpressure is the mechanism to slow the producer down. Without it, queues grow unbounded until the system OOMs.
Options: block the producer (bounded queue), drop messages (load shedding), sample (rate limiting), or signal the producer to slow down (reactive streams, TCP window).
Queue-Based Decoupling
Place a message queue (Kafka, RabbitMQ, SQS) between components to absorb traffic spikes. The queue acts as a shock absorber — producers can burst at 10x the normal rate as long as the average rate doesn't exceed consumer capacity. Also enables independent scaling and fault isolation.
22 Serialization Performance
Serialization is converting in-memory objects to bytes for storage or transmission. It happens on every API call, every database query, every cache operation. Even small improvements compound massively.
Format Comparison
| Format | Type | Size (relative) | Encode Speed | Decode Speed | Schema | Human-Readable |
|---|---|---|---|---|---|---|
| JSON | Text | 1.0x (baseline) | Moderate | Moderate | Optional | Yes |
| Protocol Buffers | Binary | 0.3-0.5x | Fast | Very Fast | Required | No |
| MessagePack | Binary | 0.5-0.7x | Fast | Fast | Optional | No |
| Avro | Binary | 0.3-0.5x | Fast | Fast | Required | No |
| FlatBuffers | Binary | 0.4-0.6x | Very Fast | Zero-copy | Required | No |
Protobuf is typically 2-10x faster than JSON for serialization/deserialization and produces 2-3x smaller payloads. For internal service-to-service communication, there's rarely a reason to use JSON.
Compression: Speed vs Ratio
| Algorithm | Ratio | Compress (MB/s) | Decompress (MB/s) | Use Case |
|---|---|---|---|---|
| LZ4 | 2.1x | 780 | 4,220 | Real-time, IPC, logs |
| Snappy | 1.8x | 500 | 1,600 | Database storage (Bigtable, Cassandra) |
| Zstd (level 1) | 2.9x | 500 | 1,380 | All-purpose replacement for gzip |
| Zstd (level 19) | 3.5x | 5 | 1,380 | Archival, slow compress OK |
| Gzip | 3.2x | 50 | 400 | HTTP (universal support) |
| Brotli (level 11) | 4.5x | 3 | 400 | Pre-compressed web assets |
Rule of thumb: Use Zstd unless you have a specific reason not to. It offers the best speed-to-ratio trade-off, with a tunable compression level from "faster than LZ4" to "smaller than gzip."
23 JVM Performance
The JVM is one of the most sophisticated runtime environments ever built. Understanding its internals unlocks performance that rivals C/C++ for many workloads — but misconfiguration can make it surprisingly slow.
GC Tuning: G1 vs ZGC
G1GC Default
- Good for heaps 4-64 GB
- Pause target:
-XX:MaxGCPauseMillis=200 - Typical pauses: 50-200ms
- Throughput-oriented with pause target
- Battle-tested, well understood
ZGC Low Latency
- Handles heaps up to 16 TB
- Pause times: <1ms (usually <0.5ms)
- Concurrent everything
- ~5-15% throughput cost vs G1
- Production-ready since Java 17
Heap Sizing
Too small: frequent GC, high GC overhead (>10% of CPU time), OutOfMemoryError risk.
Too large: longer GC pauses (for G1), wasted memory, longer startup.
Rule of thumb: set heap so live data occupies 30-50% of the heap. Monitor GC logs to tune. Use -Xms equal to -Xmx to avoid resize pauses.
JIT Warmup
The JVM starts by interpreting bytecode. After ~10,000 invocations, the JIT compiler (C2) compiles hot methods to optimized native code. This warmup period can last 30-60 seconds in large applications. Strategies:
- Accept warmup and design SLAs around it (route traffic gradually after deploy)
- Use CDS (Class Data Sharing) and AppCDS to speed up startup
- GraalVM native-image for ahead-of-time compilation (eliminates warmup but loses peak JIT performance)
- JVM warmup scripts that exercise hot paths before accepting traffic
Escape Analysis
The JIT compiler can prove that an object never "escapes" the method that created it. Such objects can be allocated on the stack (free) instead of the heap (GC-managed). This is why small, short-lived objects in hot loops are often "free" in Java — the JIT eliminates the allocation entirely.
JFR (Java Flight Recorder)
Built into the JVM since Java 11 (free in OpenJDK). Low overhead (~1-2%) continuous profiling: CPU, memory allocation, GC events, thread states, I/O, lock contention. Enable with -XX:StartFlightRecording=duration=60s,filename=recording.jfr. Analyze with JDK Mission Control (JMC). The best "always on" profiling tool for production JVMs.
Other JVM Tricks
- String deduplication (
-XX:+UseStringDeduplicationwith G1) — if your heap is 30%+ String objects (common in web apps), this can save significant memory - Compressed oops — enabled by default for heaps <32GB. Use object references as 32-bit offsets instead of 64-bit pointers. Stay under 32GB heap if possible.
- Class loading overhead — thousands of classes = slow startup. Minimize dependencies. Use jlink to create minimal runtime images.
Architecture Level
System-wide patterns that determine performance at scale
24 Scaling Patterns
Scaling is the art of handling more load without degrading performance. The approach depends on what's bottlenecked — CPU, memory, I/O, or network.
Vertical vs Horizontal Scaling
Vertical (Scale Up)
- Bigger machine: more CPU, RAM, faster disk
- Simple — no code changes
- Hard ceiling — biggest machine has limits
- Single point of failure
- Best for: databases, stateful services
Horizontal (Scale Out)
- More machines running identical software
- Near-infinite ceiling
- Fault tolerant
- Requires stateless design
- Best for: web servers, API gateways, workers
Stateless Services
The prerequisite for horizontal scaling. If a service stores no local state, any instance can handle any request. Session state moves to Redis or a cookie. File uploads go to S3. All shared state lives in a database. Now you can auto-scale from 2 to 200 instances transparently.
Sharding
Scaling Patterns Summary
| Pattern | Scales | Complexity | Use When |
|---|---|---|---|
| Read replicas | Reads | Low | Read-heavy workloads (90%+ reads) |
| Sharding | Reads + Writes | High | Single DB can't handle write volume |
| CQRS | Reads + Writes | Medium | Read and write models differ significantly |
| Event Sourcing | Writes | High | Audit trail needed, high write volume |
| Auto-scaling | Compute | Low | Variable traffic patterns |
| CDN | Static content | Low | Global audience, static assets |
25 Performance Anti-Patterns
These are the most common performance mistakes. Each one is easy to commit and can cause 10-1000x slowdowns. If your system is slow, start by checking for these.
The Hall of Shame
1. Chatty APIs (N+1 Over Network)
Making N individual API calls instead of 1 batch call. If each call has 5ms network latency, 100 calls = 500ms of pure waiting. Solution: batch endpoints, GraphQL, or eager loading.
2. Synchronous Chains
Service A calls B calls C calls D — sequentially. Total latency = sum of all latencies. If any link is slow, everything is slow. Solution: parallelize independent calls, use async where possible, set timeouts.
Total: 50ms serial. Could be 20ms if B, C, D run in parallel.
3. Unbounded Queues
A queue without a size limit grows until memory is exhausted. Under load spikes, the queue absorbs the spike... into your RAM. System slows, GC goes crazy, then OOM. Always bound your queues and implement backpressure.
4. Missing Indexes
The #1 database performance problem. A missing index turns an O(log n) lookup into an O(n) table scan. On a table with 10 million rows, that's the difference between 0.1ms and 10 seconds. Run EXPLAIN ANALYZE on your slow queries.
5. Logging in Hot Paths
Structured logging with JSON serialization in a loop processing millions of events. Each log line: JSON encode, string formatting, I/O write. Solution: log at appropriate levels (DEBUG off in prod), use sampling for high-frequency events, async logging.
6. Regex in Loops
Compiling a regex on every iteration instead of once. Regex compilation can take 100x longer than the match itself. Always compile regex patterns outside loops and reuse them.
7. Exception-Driven Control Flow
Using try/catch for expected conditions (e.g., checking if a key exists by catching KeyNotFoundException). Exception creation captures a stack trace — expensive (1-10 us). Check first, catch exceptions only for genuinely exceptional cases.
8. Premature Abstraction Overhead
Seven layers of interfaces, factories, and decorators for a simple operation. Each virtual method call is a pointer chase (cache miss). Each layer adds object allocations. Abstraction has a cost — pay it when you get value from it, not "just in case."
26 Performance Culture
Performance is not a one-time project — it's a culture. Without ongoing investment, systems get slower with every release. Building performance into your engineering culture is the only way to stay fast at scale.
Performance Budgets
Define hard limits: "The /api/search endpoint must respond within 100ms at P95." Every feature that adds latency must justify the cost. If the budget is exceeded, the feature is reverted or optimized before merging. Google, Shopify, and Netflix all use performance budgets extensively.
Regression Detection in CI
- Establish a baseline — run benchmarks on a dedicated (not shared) machine. Record P50, P95, P99 for key operations.
- Run on every PR — compare the PR's benchmarks against the baseline. Flag regressions >5% as warnings, >15% as failures.
- Track trends — store benchmark results over time. A 1% regression per week = 67% slower in a year. Trend detection catches slow degradation that per-PR checks miss.
- Automate alerts — post benchmark results as PR comments. Make performance visible to every engineer, every day.
Continuous Profiling
What It Is
Always-on, low-overhead profiling in production. Captures CPU profiles, memory allocations, and lock contention continuously. Lets you see what production code is actually doing, not what you think it's doing.
Tools
- Pyroscope — open source, push/pull model
- Datadog Continuous Profiler — integrated with APM
- Google Cloud Profiler — free for GCP workloads
- Parca — open source, eBPF-based
- Grafana Pyroscope — integrated with Grafana stack
Capacity Planning
Use current metrics + growth projections to anticipate when you'll need more capacity. Little's Law is your friend: if traffic grows 10% monthly and you're at 60% utilization, you have ~5 months before you hit 100%. Plan capacity 3-6 months ahead. Factor in seasonal peaks (Black Friday, back-to-school).
Performance Review Checklist
Before every major release, walk through this checklist:
- Have all new endpoints been load-tested?
- Are there new database queries? Have they been EXPLAIN ANALYZE'd?
- Are new indexes needed?
- Does the change add new network calls to the hot path?
- Are caches properly invalidated?
- Has memory usage been profiled under load?
- Are there unbounded collections or queues?
- Are timeouts and circuit breakers configured for new external calls?
- Do monitoring dashboards cover the new functionality?
- Are SLOs defined and alerting configured?
"Performance is a feature. Treat it like one — with requirements, testing, monitoring, and dedicated ownership." — Every engineering team that has been paged at 3am for a latency spike