Performance in Go: Measure, Profile, Then Optimize
Go is fast enough for most services without heroic tuning.
Search across all documentation pages
Go is fast enough for most services without heroic tuning.
When latency or cost matter, the language culture is explicit: prove the bottleneck with data, fix the proven hot path, and keep code readable everywhere else.
Performance Basics collects runnable measurement snippets; sibling articles cover pprof, tracing, escape analysis, allocation patterns, PGO, and GC tuning.
testing.B), pprof (CPU/heap/mutex/block), execution tracer (go tool trace), escape analysis, allocations, GOGC/GOMEMLIMIT, PGO.unsafe and pools add maintenance cost that must beat measured gains.Go programs spend time in three broad buckets: your code, the runtime (scheduler, GC, reflection), and the OS (syscalls, network, disk).
Optimization starts by identifying which bucket dominates for your workload.
Benchmarks (go test -bench) answer micro-questions: is implementation A faster than B on a fixed input size?
They run in a controlled process with warm CPU caches and are ideal for serialization, parsing, and tight loops.
Profiles (runtime/pprof, net/http/pprof) answer macro-questions: which functions consume CPU or heap under production-like concurrency?
CPU profiles are statistical samples of the call stack.
Heap profiles show where allocations originate and how much live memory each call site retains.
Traces (runtime/trace) capture a timeline: goroutine scheduling, GC STW events, syscall blocking, and network waits.
Use traces when pprof shows low CPU but latency is still high.
The compiler inlines small functions, eliminates bounds checks when it can prove safety, and applies escape analysis to decide stack vs heap allocation.
You influence it with idiomatic code more often than with //go:noinline directives.
The garbage collector is generational and concurrent.
Allocation rate drives GC CPU and pause behavior.
Reducing pointers and reusing buffers often beats turning GOGC knobs.
A typical investigation loop looks like this:
SLO alert or bench regression
|
v
Reproduce with realistic load / payload size
|
+-----+-----+
| |
v v
go test HTTP/gRPC
-bench load + pprof
| |
+-----+-----+
v
Identify hot func or alloc site
|
v
One change + recorded before/after
|
v
Ship if SLO/clarity trade-off is documented
| Tool | Question it answers | Typical signal |
|---|---|---|
go test -bench | Is this function faster? | ns/op, allocs/op |
| CPU pprof | Where is CPU time? | Flat % in one func |
| Heap pprof | Who allocates? | alloc_space, inuse_space |
go tool trace | Why goroutines wait? | Long syscall or GC bars |
go build -gcflags=-m | Does value escape? | moved to heap lines |
PGO (default.pgo) | Steady-state hot paths? | Compiler inlining hints |
Benchmarks and profiles complement each other.
A benchmark might show 30% faster JSON encoding, while a heap profile reveals the win came from fewer allocations that also reduced GC pause.
Always pair timing with -benchmem when allocations are plausible.
For services, expose net/http/pprof on an admin port or pull profiles with go tool pprof http://host:6060/debug/pprof/profile.
Collect profiles during load, not on an idle process, or the sample will be empty noise.
Latency-sensitive services (payments, auth, realtime APIs) care about tail percentiles, not average CPU.
Traces expose queueing behind mutexes, channel backpressure, and STW GC segments that averages hide.
Fix contention and allocation first; only then experiment with GOGC or GOMEMLIMIT.
Throughput workers (ETL, batch indexers) often bottleneck on IO.
CPU profiles may show syscall or compress dominance.
Parallelism, buffer sizing, and fewer copies beat shaving nanoseconds in hash functions.
Profile-Guided Optimization (PGO) feeds production CPU profiles into the compiler via default.pgo.
It helps stable binaries where hot paths do not change every commit.
Regenerate the profile on release branches, not from a one-off dev laptop trace.
Escape analysis explains why a small struct still allocates: returning a pointer to a local, storing into an interface{}, or capturing a variable in a closure that outlives the stack frame.
Sometimes the fix is returning by value; sometimes it is accepting one allocation as cheaper than fighting the compiler.
When to stop: SLOs are green, pprof flat graphs are spread across many small functions, and further changes harm readability.
Record that decision in the PR or runbook so the next engineer does not re-open settled tuning.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Benchmarks | Fast, CI-friendly | Synthetic input risk | Libraries, encoders |
| pprof | Ground truth under load | Needs representative traffic | Services |
| Trace | Scheduler/GC visibility | Larger artifacts | Tail latency mysteries |
| PGO | Compiler sees prod paths | Profile drift | Long-lived binaries |
| GC knobs | Quick experiments | Masking alloc bugs | Last resort after alloc fixes |
go test -bench alone proves production performance - Bench environment differs from networked, contended, and cached production paths. Use it to compare implementations, then validate with load tests.go tool trace when CPU pprof is flat.go tool pprof -top and flame graphs in the web UI are enough for most first passes.Reproduce with production-sized payloads and concurrency.
Capture a CPU profile and a heap profile under that load, then read the top few functions before editing code.
Benchmarks help handler helpers and serializers.
End-to-end latency needs load tests plus pprof or tracing on the running server.
CPU profiles show where time is spent executing code.
Heap profiles show allocation sites and retained memory, which often drive GC cost.
Use it when CPU usage looks healthy but requests still queue or stall.
Traces reveal scheduling, syscall, and GC timing on a timeline.
Green Tea GC is the default collector in recent releases, but the measure-profile-optimize loop is unchanged.
Re-benchmark after toolchain upgrades because compiler and runtime behavior evolve.
No.
Run go build -gcflags=-m on a package when a benchmark shows unexpected allocations.
The compiler tells you which values escape to the heap.
When you ship long-lived binaries with stable hot paths and want the compiler to inline and layout code using production profiles.
Skip PGO for fast-moving code paths or CLI tools with diverse inputs.
Usually no.
Lower allocation pressure fixes root cause; GOGC experiments are for latency after alloc work is exhausted.
Yes for benchmarks and regression guards.
Full pprof under load belongs in staging or controlled production windows with auth on debug endpoints.
Performance Rules: When to Optimize is the policy cheatsheet.
This section is the hands-on tooling guide for executing that policy.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026