Profiling & Bottleneck Analysis
Profiling turns 'the server is slow' into a ranked list of hotspots: CPU samples, flame graphs, memory and GC analysis, thread dumps and distributed traces. Learn to measure first, fix the constraint, and prove the improvement.
What is this?
Profiling and bottleneck analysis is the detective work after a load test: instead of guessing, you measure where CPU time, memory, locks, I/O and database time actually go under load. The toolchain spans CPU profilers (perf, pprof, async-profiler), heap and GC analysis, thread and lock dumps, and distributed tracing to correlate an endpoint's latency with its backend calls. The output is a ranked list of hotspots and a root-cause fix — not a vague 'the server is slow'.
Why do you need it?
- Latency and error-rate numbers tell you something is wrong; profiles tell you exactly what — the difference between symptoms and causes.
- CPU hotspots, lock contention, GC pressure and I/O waits each need different fixes, and only measurement tells them apart.
- A profile taken during the load test is worth a thousand after-the-fact guesses; transient bottlenecks vanish once load stops.
- Bottleneck analysis turns capacity planning from intuition into a model: fix the constraint and capacity improves proportionally.
- Distributed tracing ties a slow page to the specific downstream call, so teams stop firefighting the wrong service.
Mental model
A doctor running tests instead of guessing
Profiling a slow system
You don't treat 'fever' generically — you run blood work, cultures and imaging to find the infection. Profilers and traces are the equivalent diagnostics for a slow service.
How it works
- Reproduce the load that triggers the symptom and keep it running while you collect samples.
- Capture a CPU profile (perf record, async-profiler or pprof) and visualize it as a flame graph.
- Snapshot memory and GC behavior: heap dumps, GC logs and allocation profiles reveal leaks and allocation churn.
- Correlate with blocking: thread dumps and lock contention show stalls that CPU time cannot.
- For distributed systems, follow a slow trace: span-by-span time reveals the downstream culprit.
- Confirm the fix by re-running the same load test and comparing the profile and metrics to the baseline.
From a slow endpoint to a root-cause fix.
Key commands
perf record -F 99 -g -- ./my-serviceSample the process at 99 Hz and record call stacks for flame graph generation.
perf reportInteractive tree view of recorded samples, ranked by cost.
Overhead Command Shared Object Symbol 12.4% server libc.so.6 memcpy
top -H -p <pid>Watch per-thread CPU and identify the busiest thread or goroutine.
jcmd <pid> GC.heap_infoDump JVM heap status and GC behavior without restarting the application.
jstack <pid> > threaddump.txtCapture a Java thread dump to spot deadlocks and blocked threads.
go tool pprof -top http://localhost:6060/debug/pprof/profileProfile a Go service over HTTP and show the top hotspots directly.
Examples
Profiling a Go service under load with pprof
# Sample the CPU for 30 seconds while load is runninggo tool pprof -seconds 30 http://localhost:6060/debug/pprof/profile # Rank functions by self timego tool pprof -top /tmp/pprof.pprof.go1.20.cpu.001.pb.gz # Render a flame graphgo tool pprof -svg /tmp/pprof.pprof.go1.20.cpu.001.pb.gz > cpu.svg # Blocking/contention profile (goroutine stalls)go tool pprof http://localhost:6060/debug/pprof/blockpprof samples the running Go service's CPU for 30 seconds while the load test is in flight. -top ranks functions by self time to find the hotspot, and -svg renders a flame graph you can inspect visually. The block profile shows goroutine stalls that CPU time alone won't reveal.
In production
- A flame graph from a staging load test turns a vague 'slow endpoint' into a named function call to optimize.
- GC logs and heap profiles reveal allocation churn that causes throughput collapse under load.
- Thread dumps taken at the moment of a latency spike catch locks and pool stalls that disappear when load stops.
- Per-request tracing attributes latency to the actual downstream dependency before any code is touched.
Best practices
- Profile while the load is still running — a post-mortem profile after traffic stops misses the bottleneck.
- Collect CPU, memory and blocking data in parallel; the same symptom often has more than one contributing factor.
- Compare profiles before and after a fix, not just 'is it faster now'.
- Profile in an environment that matches production — same CPU, GC settings and data volume.
- Reduce the search space with traces first: know which service and span is slow before deep-profiling one binary.
Common mistakes
- Profiling a single request in isolation and assuming the hotspot is the same under concurrency — locks only appear with load.
- Reading the widest flame graph frame and 'fixing' it without checking self time versus cumulative time.
- Optimizing CPU when the real constraint is I/O or connection-pool exhaustion — measured, not guessed.
- Killing the load before capturing the profile, so the sampled workload doesn't match the symptom.
Common issues
perf record failed to read the process / Operation not permittedperf needs elevated privileges or a permissive perf_event_paranoid setting.
Run with sudo, or set /proc/sys/kernel/perf_event_paranoid to -1 and verify the kernel config.
profile has 0 samplesThe app exited, or sampling happened outside the slow window.
Keep the load running and use -seconds to capture a window inside it; verify the PID.
flame graph is dominated by wait / blocked framesThe bottleneck is contention or I/O, not CPU.
Collect lock and block profiles (pprof/block, jstack) and trace downstream calls instead.
Debugging tips
- A flat CPU profile with high latency points to waiting (I/O, locks, pool) rather than computation.
- Compare the profile during steady state versus during the spike — different hotspots mean different fixes.
- Watch GC throughput (bytes/sec) alongside latency; heavy GC usually correlates with p99 cliffs.
- Use the profile timestamps: only trust samples taken inside the observed slow window.
Interview checkpoint
Hands-on lab
- Run a k6 load test against a deliberately CPU-heavy Node service
- Capture a CPU profile while the load is in flight
- Read the flame chart to identify the hotspot
- Apply a fix and prove the improvement with the identical workload
Profile a Bottleneck Under Load
40 min · advanced
Quick quiz
Which tool samples a Go service's CPU over HTTP while it is under load?
A flame graph's widest frame at the top is always the code you should optimize.
Latency is high but CPU utilization is near zero. Which hypothesis is most likely?
Cheat sheet
perf record -F 99 -g -- <cmd>CPU samples + call stacksperf reportRanked hotspot treego tool pprof -seconds 30 :6060/debug/pprof/profileGo CPU profile over HTTPjstack <pid>Java thread dumpjcmd <pid> GC.heap_infoJVM heap + GC statetop -H -p <pid>Per-thread CPU watch