← Knowledge Base

Performance

A deep practical guide to making software fast — from CPU caches to system architecture.

26 Sections Real Numbers Practical Focus Deep Dive

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 measuring — Your intuition about bottlenecks is wrong 90% of the time. Profile first.

🛠

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).

PercentileMeaningWhy It Matters
P50Median — half of requests are fasterTypical user experience
P9090th percentile1 in 10 requests is slower than this
P9595th percentileCommon SLA target
P9999th percentileTail latency — often 5-50x the median
P99.999.9th percentile1 in 1000 — matters at scale (1M req = 1000 affected)
Histogram View: Percentile View: Count Latency | | |### | _________ P99.9 |##### | ______/ |######## | ______/ P99 |########### | _____/ |############## | ___/ P95 |################# | / P50 |###################### |/________________________________ +--------------------> Latency +----> % of requests

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.

┌─────────────────────────────────────────────────────────────────────┐ │ LATENCY NUMBERS (approximate) │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ L1 cache reference ................... 1 ns │ │ Branch mispredict .................... 3 ns │ │ L2 cache reference ................... 4 ns 4x L1 │ │ Mutex lock/unlock ................... 17 ns │ │ L3 cache reference .................. 12 ns 12x L1 │ │ Main memory reference .............. 100 ns 100x L1 │ │ Compress 1KB with Snappy ......... 3,000 ns 3 us │ │ Read 1MB sequentially (RAM) ..... 10,000 ns 10 us │ │ SSD random read ................. 16,000 ns 16 us │ │ Read 1MB sequentially (SSD) ... 100,000 ns 100 us │ │ HDD seek ................... 10,000,000 ns 10 ms │ │ Read 1MB sequentially (HDD) 30,000,000 ns 30 ms │ │ Round trip same datacenter ..... 500,000 ns 500 us 0.5 ms │ │ Round trip CA -> Netherlands 150,000,000 ns 150 ms │ │ │ └─────────────────────────────────────────────────────────────────────┘

Visual Scale (logarithmic, 1 ns = 1 second analogy)

If L1 cache = 1 second: L1 cache ............ 1 second L2 cache ............ 4 seconds L3 cache ............ 12 seconds RAM ................. 1 min 40 sec SSD random read ..... 4 hours 26 min SSD sequential 1MB .. ~1 day 3 hours HDD seek ............ ~4 months Round-trip DC ........ 5 days 18 hours Round-trip internet .. ~4.75 years

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.

Speedup vs Processors (Amdahl's Law) Speedup 20x | .............. S=5% | ............ 16x | ......... | ..... 12x | .... | ... _______________ S=10% 8x | .. ______/ | .. ______/ 4x | . _______/ ___________________ S=25% | . ______/ ______/ 2x | . ____/ _______/ | . __/ _______/ ______________________________ S=50% 1x |./____._____./______._______/________________________________ +--+-----+-----+-----+-----+-----+-----+-----> 1 4 8 16 32 64 128 256 Processors

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

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.

Average Wait Time vs Utilization (M/M/1 Queue) Wait Time (multiple of service time) | 10x | . | .. 8x | .. | .. 6x | .. | .. 4x | ... | ... 2x | ..... | ......... 1x | ............... +--------+--------+--------+--------+--------> 0% 20% 40% 60% 80% 100% Utilization

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
vs

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

ToolLanguageTypeProduction-SafeNotes
async-profilerJavaSamplingYesGold standard for JVM. Handles native frames.
VisualVMJavaBothCautionGood GUI, attach to running process
perfC/C++/SystemSamplingYesLinux kernel profiler. Hardware counters.
pprofGoSamplingYesBuilt into Go runtime. CPU, memory, goroutine.
Chrome DevToolsJavaScriptSamplingCautionCPU, memory, network waterfall
py-spyPythonSamplingYesNo code changes needed. Attaches to running process.
rbspyRubySamplingYesSimilar to py-spy for Ruby
dotTrace.NETBothCautionJetBrains. Excellent timeline view.

Memory Profiling

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.
Example Flame Graph (CPU) ┌───────────────────────────────────────────────────────────────┐ │ main() │ ├───────────────────────────────────┬───────────────────────────┤ │ handleRequest() │ backgroundJob() │ ├──────────────────┬────────────────┤ │ │ parseJSON() │ queryDB() ├────────────┬──────────────┤ │ ├──────┬─────────┤ compress() │ serialize() │ │ │wait()│execute()│ │ │ └──────────────────┴──────┴─────────┴────────────┴──────────────┘ ←───────────────── x-axis = % of samples (wider = more time) ──> Reading: queryDB() is the widest frame. Its children show time split between wait() (I/O wait) and execute() (actual computation). parseJSON() is also significant. compress() is a smaller cost.

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

  1. Java: async-profiler -d 30 -f profile.html <pid> — produces an interactive HTML flame graph directly.
  2. Go: go tool pprof -http=:8080 cpu.prof — built-in flame graph view in the web UI.
  3. Linux (any language): perf record -g -F 99 -p <pid> -- sleep 30, then perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg
  4. 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)
vs

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

ToolLanguageStrengthsUse When
wrkCVery fast, Lua scriptingMax throughput testing
wrk2CCorrects coordinated omissionAccurate latency measurement
k6GoJavaScript test scripts, scenariosComplex load patterns
heyGoSimple, fastQuick smoke tests
abCUbiquitous (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

TypePurposePatternDuration
SmokeVerify system works under minimal load1-5 users1-5 min
LoadValidate expected traffic levelsNormal peak load15-60 min
StressFind the breaking pointRamp to 2-3x peak15-30 min
SpikeTest sudden traffic burstsInstant jump to 10x5-10 min
SoakFind memory leaks, resource exhaustionNormal load, extended4-24 hours
Load Test Patterns: Smoke Load Stress Spike Soak ───── ──── ────── ───── ──── Users Users Users Users Users | | ─── | ╱ | ╱╲ | ─────────── | ── | ╱ ╲ | ╱ ╲ | ╱ ╲ | ╱ | | ╱ ╲ | ╱ ╲ | ╱ ╲ | ╱ | | ╱ ╲ | ╱ ╲ | ╱ ╲ |╱ +────> time +─────> time +──────> time +────> time +─────────> time

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

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)

Rate (req/s)
→
Errors (%)
→
Duration (latency)

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)

Utilization (%)
→
Saturation (queue depth)
→
Errors (HW/SW)

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

ConceptDefinitionExample
SLI (Indicator)A measurement of service healthP99 latency of the /api/search endpoint
SLO (Objective)A target value for an SLIP99 latency < 200ms for 99.9% of requests
SLA (Agreement)A contract with consequencesIf P99 > 200ms for >0.1% of requests, customer gets credit
Error BudgetAllowed failures before SLO breach99.9% SLO = 43.8 min of downtime per month

Prometheus + Grafana Stack

App exports metrics
→
Prometheus scrapes
→
PromQL queries
→
Grafana dashboards
→
Alertmanager

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

Classical 5-Stage Pipeline: Fetch → Decode → Execute → Memory Access → Write Back Modern CPUs: 14-20+ stages, out-of-order execution, speculative execution. Throughput: up to 4-6 instructions per cycle (superscalar). When pipeline stalls (cache miss, branch mispredict), ALL stages are wasted.

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:

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

┌──────────────────────────────────────────────────────────────────┐ │ CPU Core │ │ ┌──────────────┐ │ │ │ L1 Cache │ 32-64 KB ~1 ns ~4 cycles │ │ │ (per core) │ │ │ └──────┬───────┘ │ │ ┌──────┴───────┐ │ │ │ L2 Cache │ 256 KB-1MB ~4 ns ~12 cycles │ │ │ (per core) │ │ │ └──────┬───────┘ │ ├─────────┴────────────────────────────────────────────────────────┤ │ ┌──────────────────────────┐ │ │ │ L3 Cache (shared) │ 8-64 MB ~12 ns ~40 cycles │ │ └──────────┬───────────────┘ │ ├─────────────┴────────────────────────────────────────────────────┤ │ ┌──────────────────────────────────────┐ │ │ │ Main Memory (DRAM) │ GBs ~100 ns ~300+ │ │ └──────────────────────────────────────┘ cycles │ └──────────────────────────────────────────────────────────────────┘

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)

ComplexityNameN=10N=1,000N=1,000,000Real-World Example
O(1)Constant111HashMap lookup
O(log n)Logarithmic31020Binary search, B-tree lookup
O(n)Linear101,0001,000,000Array scan, single loop
O(n log n)Linearithmic3310,00020,000,000Sorting (mergesort, quicksort)
O(n²)Quadratic1001,000,00010¹²Nested loops, naive string matching
O(2ⁿ)Exponential1,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:

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.

Thread-per-request vs Event Loop at high concurrency: Throughput (req/s) | | Event Loop | ───────────────────────────────────── | / | / Thread-per-request | / ─────────────── |/ ╲ | ╲ (context switching | ╲ overwhelms the CPU) +──────────────────────────────> 10 100 1K 10K 100K Concurrent connections

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

LevelSize (typical)LatencyBandwidthAnalogy
L1 Cache32-64 KB per core~1 ns (4 cycles)~1 TB/sYour desk
L2 Cache256 KB - 1 MB per core~4 ns (12 cycles)~500 GB/sFiling cabinet
L3 Cache8 - 64 MB shared~12 ns (40 cycles)~200 GB/sOffice supply room
DRAM16 - 512 GB~100 ns (300 cycles)~50 GB/sWarehouse 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).

vs

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

  • Uncleared collections — a HashMap that grows but never has entries removed (event listeners, caches without TTL)
  • Closures capturing references — a lambda/closure holds a reference to a large object, preventing GC
  • Thread-local storage — thread pool reuses threads, so ThreadLocal values persist for the pool's lifetime
  • Unclosed resources — streams, connections, file handles that are opened but never closed in error paths
  • ClassLoader leaks — in Java app servers, redeployment without proper cleanup keeps old classes alive
  • 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

    Choosing the Right Structure

    Access PatternBest StructureWhy
    Key-value lookupHashMap / dictO(1) average lookup
    Sorted key-valueB-tree / TreeMapO(log n) lookup + range queries
    Membership testHashSetO(1) contains
    Probabilistic membershipBloom filterO(k) with ~1% false positives, zero false negatives, tiny memory
    Priority queueBinary heapO(log n) insert/extract-min
    FIFO queueArrayDequeO(1) amortized, cache-friendly (not LinkedList!)
    Append-heavy listArrayList / VecO(1) amortized append, cache-friendly iteration
    Concurrent key-valueConcurrentHashMapLock striping, high concurrency
    Range queries on diskB+ tree (DB index)Minimal disk seeks, high fan-out
    Write-heavy on diskLSM treeSequential 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

    OperationHDDSSD (SATA)SSD (NVMe)RAM
    Sequential read (MB/s)100-200500-5503,000-7,00030,000+
    Random read (4K IOPS)100-20075,000-100,000500,000-1,000,000millions
    Random read latency5-10 ms50-100 us10-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

    Write arrives
    →
    Append to WAL (sequential)
    →
    Acknowledge to client
    →
    Apply to data structure (async)

    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

    HTTP/1.1 (6 connections, head-of-line blocking): Conn 1: [───Req A───][───Req G───] Conn 2: [───Req B───][───Req H───] Conn 3: [───Req C─────────────────] (slow response blocks this conn) Conn 4: [───Req D───][───Req I───] Conn 5: [───Req E───] Conn 6: [───Req F───] HTTP/2 (1 connection, multiplexed streams): Conn 1: [A][B][C][D][A][E][B][F][C][D][G][H][I]... (interleaved frames, no head-of-line blocking at HTTP level)

    Compression Trade-offs

    AlgorithmCompression RatioCompress SpeedDecompress SpeedBest For
    LZ42.0-2.5x~780 MB/s~4,000 MB/sSpeed-critical (logs, IPC)
    Snappy1.5-2.0x~500 MB/s~1,500 MB/sSpeed-critical (Bigtable)
    Zstd2.5-3.5x~400 MB/s~1,200 MB/sBest all-rounder
    Gzip3.0-4.0x~50 MB/s~400 MB/sHTTP (universal support)
    Brotli3.5-5.0x~20 MB/s~400 MB/sStatic 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

    The N+1 Query Problem: BAD (N+1 queries): SELECT * FROM orders WHERE user_id = 42; -- 1 query SELECT * FROM products WHERE id = 101; -- N queries SELECT * FROM products WHERE id = 102; -- (one per SELECT * FROM products WHERE id = 103; -- order item) ... (100 more queries) GOOD (2 queries): SELECT * FROM orders WHERE user_id = 42; -- 1 query SELECT * FROM products WHERE id IN (101,102,...); -- 1 query BEST (1 query with JOIN): SELECT o.*, p.* FROM orders o JOIN products p ON o.product_id = p.id WHERE o.user_id = 42; -- 1 query Impact: 100 queries × 0.5ms network RTT = 50ms vs 1ms

    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

    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

    Browser Cache
    →
    CDN
    →
    Reverse Proxy
    →
    App Cache
    →
    DB Cache
    →
    Origin

    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.

    Check cache
    →
    Miss? Query DB
    →
    Store in cache

    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

    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

    Event Loop (Node.js, Netty, Nginx, Tokio): ┌─────────────────────────────────────────────┐ │ Event Loop │ │ │ │ ┌─────────┐ ┌─────────┐ │ │ │ Poll for │ │ Execute │ │ │ │ I/O │───>│ callback│──┐ │ │ │ events │ │ │ │ │ │ └─────────┘ └─────────┘ │ │ │ ↑ │ │ │ └────────────────────────┘ │ │ │ │ Single thread handles 10,000+ connections │ │ by never blocking — just registers interest│ │ in I/O events and reacts when they occur. │ └─────────────────────────────────────────────┘

    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.

    Producer (fast)
    →
    Bounded Queue
    →
    Consumer (slow)

    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

    FormatTypeSize (relative)Encode SpeedDecode SpeedSchemaHuman-Readable
    JSONText1.0x (baseline)ModerateModerateOptionalYes
    Protocol BuffersBinary0.3-0.5xFastVery FastRequiredNo
    MessagePackBinary0.5-0.7xFastFastOptionalNo
    AvroBinary0.3-0.5xFastFastRequiredNo
    FlatBuffersBinary0.4-0.6xVery FastZero-copyRequiredNo

    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

    AlgorithmRatioCompress (MB/s)Decompress (MB/s)Use Case
    LZ42.1x7804,220Real-time, IPC, logs
    Snappy1.8x5001,600Database storage (Bigtable, Cassandra)
    Zstd (level 1)2.9x5001,380All-purpose replacement for gzip
    Zstd (level 19)3.5x51,380Archival, slow compress OK
    Gzip3.2x50400HTTP (universal support)
    Brotli (level 11)4.5x3400Pre-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
    vs

    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:

    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

    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
    vs

    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

    Sharding by User ID: Request (user_id=42) ──> shard = hash(42) % 4 = 2 ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Shard 0 │ │ Shard 1 │ │ Shard 2 │ │ Shard 3 │ │ users │ │ users │ │ users │ │ users │ │ 0,4,8... │ │ 1,5,9... │ │ 2,6,10..│ │ 3,7,11..│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ Each shard is an independent database. Cross-shard queries are expensive. Choose shard key carefully — it should distribute evenly and align with your most common query pattern.

    Scaling Patterns Summary

    PatternScalesComplexityUse When
    Read replicasReadsLowRead-heavy workloads (90%+ reads)
    ShardingReads + WritesHighSingle DB can't handle write volume
    CQRSReads + WritesMediumRead and write models differ significantly
    Event SourcingWritesHighAudit trail needed, high write volume
    Auto-scalingComputeLowVariable traffic patterns
    CDNStatic contentLowGlobal 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.

    A: 10ms
    →
    B: 15ms
    →
    C: 20ms
    →
    D: 5ms

    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

    1. Establish a baseline — run benchmarks on a dedicated (not shared) machine. Record P50, P95, P99 for key operations.
    2. Run on every PR — compare the PR's benchmarks against the baseline. Flag regressions >5% as warnings, >15% as failures.
    3. 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.
    4. 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