Stage 6 · Operate
Capacity & Performance Engineering
Performance Profiling
Flame graphs, pprof, perf, and tracing slow paths across service boundaries.
Why Profile?
Profiling reveals where your application spends time and resources. Without profiling, you are guessing about performance. Profiling shows you the exact functions, lines of code, and call paths that consume CPU, memory, and time.
Performance optimization without profiling is guesswork. You may optimize code that is not a bottleneck while ignoring the actual problem. Profile first, optimize based on data.
Flame Graphs
Flame graphs visualize where time is spent in your application. Each bar represents a function call. The width shows how much time was spent. Wide bars at the top are your hot paths — where optimization efforts should focus.
flame_graph_reading:
x_axis: "Width of bar = percentage of time"
y_axis: "Stack depth (caller above, callee below)"
hot_path: "Wide bar at the top = function spending most time"
optimization_target: "Widest bars in the middle of the stack"
common_patterns:
cpu_hot_path:
description: "Wide bar in application code"
action: "Optimize the algorithm or data structure"
gc_pressure:
description: "Wide bar in garbage collector"
action: "Reduce allocations, reuse objects"
lock_contention:
description: "Wide bar in mutex/futex"
action: "Reduce lock scope, use read-write locks"
io_wait:
description: "Wide bar in syscall/read/write"
action: "Batch I/O, use connection pools, async I/O"
tools:
go: "go tool pprof -http=:8080 profile.pb.gz"
python: "py-spy top --pid PID"
java: "async-profiler, JFR"
node: "clinic.js, 0x"Go pprof
Go has built-in profiling support through the pprof package. Enable it in your application and use go tool pprof to analyze CPU, memory, goroutine, and mutex profiles.
# Capture CPU profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Capture memory profile
go tool pprof http://localhost:6060/debug/pprof/heap
# Capture goroutine dump
go tool pprof http://localhost:6060/debug/pprof/goroutine
# Interactive analysis
go tool pprof profile.pb.gz
> top 20 # Show top 20 functions by CPU usage
> list handler # Show source code for handler function
> web # Show call graph in browser
# Generate flame graph
go tool pprof -http=:8080 profile.pb.gzDistributed Tracing
Profiling one service is not enough. In microservices, a slow request may traverse multiple services. Distributed tracing tracks a request across service boundaries to find where time is spent.
# OpenTelemetry Collector configuration
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 5s
send_batch_size: 1024
tail_sampling:
decision_wait: 10s
policies:
# Always sample errors
- name: error-policy
type: status_code
status_code:
status_codes: [ERROR]
# Sample 10% of successful requests
- name: probabilistic-policy
type: probabilistic
probabilistic:
sampling_percentage: 10
# Always sample slow requests
- name: latency-policy
type: latency
latency:
threshold_ms: 1000
exporters:
jaeger:
endpoint: jaeger.internal:14250
prometheus:
endpoint: 0.0.0.0:8889Profiling in Production
Production profiling captures real-world performance. Enable continuous profiling with low overhead (1-5% CPU). Use tools like Pyroscope, Parca, or Google Cloud Profiler for always-on profiling.
continuous_profiling:
tool: "Pyroscope"
overhead: "1-2% CPU"
storage: "S3 + PostgreSQL"
configuration:
application_name: "user-api"
server_address: "http://pyroscope.internal:4040"
sample_rate: 10 # 10 Hz
tags:
environment: "production"
region: "us-east-1"
profiling_types:
- "cpu"
- "heap"
- "goroutine"
- "mutex"
- "block"
retention:
hot: "7 days"
warm: "30 days"
cold: "90 days"
alerts:
- name: "CPU spike detected"
condition: "cpu_usage > 2x baseline for 5 minutes"
action: "Notify on-call, attach flame graph"
- name: "Memory leak detected"
condition: "heap_usage increasing for 24 hours"
action: "Notify on-call, create investigation ticket"Optimization Patterns
Common performance optimization patterns based on profiling results. Apply these after profiling confirms they address actual bottlenecks.
optimization_patterns:
caching:
when: "Profiling shows repeated expensive computations or I/O"
patterns:
- "In-memory cache with TTL"
- "Distributed cache (Redis/Memcached)"
- "HTTP cache headers"
tradeoff: "Stale data risk, memory usage"
connection_pooling:
when: "Profiling shows connection establishment overhead"
patterns:
- "Database connection pool"
- "HTTP client connection pool"
- "gRPC connection reuse"
tradeoff: "Pool size tuning, idle connection management"
batching:
when: "Profiling shows per-request I/O overhead"
patterns:
- "Batch database inserts"
- "Batch API calls"
- "Batch log writes"
tradeoff: "Latency increase, complexity"
async_processing:
when: "Profiling shows synchronous blocking"
patterns:
- "Message queues for background work"
- "Goroutines/threads for parallel processing"
- "Async I/O with callbacks/promises"
tradeoff: "Complexity, error handling",
precomputation:
when: "Profiling shows expensive computations per request"
patterns:
- "Precompute results at startup"
- "Cache computed values"
- "Use lookup tables"
tradeoff: "Memory usage, freshness"Never optimize without profiling first. The bottleneck is almost never where you think it is. Profile, identify the actual bottleneck, optimize it, then profile again to verify the improvement.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.