Performance Best Practices
Optimization decision tree and when to stop tuning.
Apply these rules in design reviews, incident postmortems, and optimization PRs so the team measures first, changes one variable at a time, and stops when SLOs are met.
How to Use This List
- Require profile or benchmark evidence before merging hot-path rewrites.
- Walk tiers A through D in order during incidents - do not jump to GC knobs.
- Encode Tier A items in service templates (pprof admin, timeouts, bench CI).
- Pair every tuning change with rollback values and SLI dashboards.
A - Measure before changing code
- Reproduce with production-sized payloads and concurrency. Synthetic tiny inputs hide real alloc and lock costs.
- Capture CPU and heap profiles under load, not on idle pods. Empty profiles waste investigation time.
- Attach
go test -bench/benchstatoutput for library changes. Micro claims need micro proof. - Record Go version,
GOMAXPROCS, and load generator settings in PRs. Numbers without context are not reproducible. - Compare one change at a time. Mixed refactors make attribution impossible.
B - Fix the right bottleneck
- Read flat CPU time in your module before editing stdlib wrappers. Cumulative time in
net/httpis often your handler. - Pair CPU profiles with heap
alloc_spacewhen GC or tail latency matters. Allocation churn drives pause frequency. - Use execution trace when CPU is low but latency is high. Scheduler, syscall, and STW waits show up on the timeline.
- Fix IO, pooling, and contention before shaving hash loops. Network and locks dominate many services.
- Run escape analysis (
-gcflags=-m) when benchmem shows surprise allocs. Compiler facts beat guessing.
C - Write allocation-aware hot paths
- Pre-size slices and maps when length is bounded.
make([]T, 0, n)and map hints reduce growth copies. - Build strings with
strings.Builderin loops. Avoid+=concatenation in hot middleware. - Prefer value semantics for small structs when APIs allow. Unnecessary pointers increase GC scan work.
- Introduce
sync.Poolonly after measured alloc wins. Pools add complexity and stale-state risk. - Stream responses instead of buffering giant
[]byte. Memory spikes become GC events under load.
D - Compiler, PGO, and GC (last resorts)
- Refresh
default.pgoon release trains for stable CPU-bound binaries. Stale profiles misguide the compiler. - Set
GOMEMLIMITnear pod memory limits before OOM incidents repeat. Soft limit triggers earlier GC, not magic capacity. - Tune
GOGCin canaries with p99 and CPU guardrails. Document rollback env vars in the runbook. - Re-benchmark after Go toolchain upgrades. Green Tea GC and inlining rules evolve between releases.
- Stop tuning when SLOs are green and further changes hurt clarity. Record the stop decision in the PR.
E - Service and operations hygiene
- Expose pprof on localhost or authenticated admin ports only. Public
/debug/pprofis a security and DoS risk. - Set HTTP client timeouts and reuse
Transportper upstream.http.DefaultClienthas no timeout. - Align
GOMAXPROCSwith container CPU quota. Runnable queue latency shows up in traces when mismatched. - Track performance regressions in staging load tests. Catch alloc regressions before production deploys.
- Treat
unsafeand//go:noinlineas reviewed exceptions. Require ADR or benchmark evidence.
When You Are Done
Tier A through C should be satisfied before Tier D experiments ship to production.
If p99 latency, error rate, and cost metrics meet agreed SLOs, defer further optimization until the next measured regression.
Clarity wins default review unless evidence shows a hot path exception.
FAQs
Which rules should block merge?
Profiles or benches for optimization-only PRs, no public pprof, and documented rollback for GC env changes are common merge blockers.
How do these relate to go-rules performance rules?
Performance Rules: When to Optimize is the cheatsheet policy.
This list is the operational checklist for teams executing that policy.
When is it OK to skip profiling?
Cosmetic or cold-path changes with no performance claim.
Any PR advertising speed or latency improvements needs evidence.
Should juniors optimize without senior review?
Measurement work is encouraged.
Merging hot-path rewrites or GC tuning should include review from someone who can read pprof and trace output.
What is the most common premature optimization?
sync.Pool and manual inlining before heap profiles prove allocation or CPU hotspots.
Measure first.
How often should we refresh PGO profiles?
When release branches cut or when CPU flat graphs shift after major features.
Stable services often refresh monthly or per release.
Do these apply to TinyGo or WASM targets?
Measure-first culture applies.
Profiling tooling differs - verify board and runtime constraints at build time per manifest pins.
Can we automate checklist items in CI?
Yes for benchmarks on critical packages, lint banning http.Get in handlers, and staging load gates.
GC tuning remains a manual canary step.
Related
- Performance in Go: Measure, Profile, Then Optimize - section overview
- Performance Basics - runnable starting examples
- CPU & Heap Profiling with pprof - evidence gathering
- Performance Rules: When to Optimize - policy cheatsheet
- GC Tuning for Latency-Sensitive Services - controlled GC experiments
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).