Go pprof Profiling skill for Codex

Right profile, right line, benchstat proof. Codex picks the pprof profile that matches the symptom, exposes net/http/pprof on a loopback-only listener rather than the public port, and reads flat versus cumulative cost down to the source line. It recognises allocation churn, map and reflection hot spots, lock contention and goroutine leaks, fixes them, and proves the change with benchstat over ten runs or a diffed profile, with PGO as a final step.

  • Performance
  • $go-pprof
  • 146 lines
  • 2 files
  • ~1,650 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- go-pprof

Installs to ~/.agents/skills/go-pprof/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $go-pprof, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Profiles Go programs with pprof - CPU, heap (inuse and alloc), goroutine, block and mutex profiles from net/http/pprof or go test, compared with -diff_base and benchstat - and fixes what they show, from allocation churn and lock contention to goroutine leaks, then feeds a production CPU profile back as PGO. Use when a Go service has high CPU or latency, memory or goroutine counts climb, a benchmark regressed, or someone asks to profile a Go binary or read a pprof output.

Use it when

  • A Go service has high CPU, latency or GC overhead
  • Goroutine or heap counts grow over time
  • A Go benchmark regressed and you need the cause

Use something else when

  • You need system-wide flame graphs across processes and the kernel: use flamegraphs
  • You are writing or reviewing Go benchmarks themselves: use benchmark-testing

How to check the result

Benchstat over at least ten runs shows a significant drop in time/op or allocs/op, and a diffed CPU profile or load test confirms the hotspot shrank.

The mistake it prevents

Importing net/http/pprof in a service that serves on DefaultServeMux, which publishes the profiling endpoints on the public port.

~/.agents/skills/go-pprof/SKILL.md

--- name: go-pprof description: Profiles Go programs with pprof - CPU, heap (inuse and alloc), goroutine, block and mutex profiles from net/http/pprof or go test, compared with… license: MIT ---

Done well means: the profile type matches the symptom, it was captured under realistic load from a binary built like production, the conclusion names a function and line with its share of samples, and the fix is proven with benchstat or a diffed profile.

Pick the profile

SymptomProfileEndpoint / flag
High CPU, slow handlersCPU/debug/pprof/profile?seconds=30, -cpuprofile
Memory high nowheap, inuse_space/debug/pprof/heap
GC busy, allocation churnheap, alloc_space / alloc_objects/debug/pprof/allocs
Goroutine count climbs, hangsgoroutine/debug/pprof/goroutine?debug=2
Latency high, CPU low, channels/IO waitsblockneeds runtime.SetBlockProfileRate
Lock contentionmutexneeds runtime.SetMutexProfileFraction
Scheduling, GC pauses, latency of one requestexecution trace/debug/pprof/trace?seconds=5, go tool trace

Expose pprof safely

import (
    "net/http"
    _ "net/http/pprof" // registers /debug/pprof/* on http.DefaultServeMux
)

func init() {
    go func() {
        // Separate, loopback-only listener: never on the public port
        _ = http.ListenAndServe("127.0.0.1:6060", nil)
    }()
}
  • If the app serves on http.DefaultServeMux, importing net/http/pprof exposes the endpoints publicly. Use a dedicated mux or loopback listener and confirm with curl -s localhost:<public-port>/debug/pprof/ that it is not reachable.
  • Reach a pod’s loopback with kubectl port-forward pod/<name> 6060 (requires cluster access; ask before touching production).
  • Block and mutex profiling cost CPU; enable with modest rates (SetBlockProfileRate(10000) in ns, SetMutexProfileFraction(100)) and only for the investigation.

Workflow

- [ ] 1. Read AGENTS.md / go.mod: Go version, build flags, how the service runs
- [ ] 2. Reproduce with load (k6, hey, vegeta) or a benchmark with fixed input
- [ ] 3. Capture the matching profile during steady load (30 s CPU)
- [ ] 4. top / list / web view: find the hot function and line
- [ ] 5. Fix one thing; capture again; compare with -diff_base or benchstat
- [ ] 6. Consider PGO with a representative production CPU profile

Reading profiles

go tool pprof -http=:8080 http://127.0.0.1:6060/debug/pprof/profile?seconds=30
go tool pprof -http=:8080 -sample_index=alloc_space http://127.0.0.1:6060/debug/pprof/allocs
go tool pprof -top -nodecount=25 cpu.pprof
go tool pprof -list 'orders\.BuildReport' cpu.pprof     # per-line costs in that function
go tool pprof -http=:8080 -diff_base=before.pprof after.pprof

In the web UI: Flame Graph for shape, Top for flat (self) vs cum (inclusive), Source for lines. flat high = this function’s own code; cum high and flat low = it calls something expensive.

Recognise common frames:

Hot framesMeaningUsual fix
runtime.mallocgc, runtime.gcBgMarkWorker, scanobjectAllocation pressureReuse buffers, preallocate slices (make([]T, 0, n)), avoid []byte↔string copies, sync.Pool for hot short-lived objects
runtime.mapassign, mapaccessMap-heavy hot loopPre-size maps, use slices for dense keys, avoid map of structs copied by value
encoding/json.*, reflect.*Reflection-based serialisationAvoid re-encoding, stream with json.Encoder, consider generated or faster codecs after measuring
runtime.convT*, runtime.assertE2IInterface boxing in hot pathConcrete types, generics in hot loops
sync.(*Mutex).Lock in mutex profileContentionShard the lock, shorten critical section, sync.RWMutex only for read-heavy, atomics for counters
syscall.Syscall, internal/poll in CPUMany small I/O callsBuffer with bufio, batch writes
regexp.(*Regexp).doExecuteRegex in a loopCompile once at package level; strings functions when possible

Escape analysis explains surprise heap allocations: go build -gcflags='-m' ./internal/orders 2>&1 | grep 'escapes to heap'.

Benchmarks with profiles

go test -run '^$' -bench 'BenchmarkBuildReport' -benchmem -count 10 \
  -cpuprofile cpu.pprof -memprofile mem.pprof ./internal/orders > old.txt
# make the change, then the same command > new.txt
go run golang.org/x/perf/cmd/benchstat@latest old.txt new.txt

Use -count 10 or more and benchstat; a single run is noise. Use b.Loop() (Go 1.24+) or b.ResetTimer() so setup is excluded. Benchstat needs network the first time.

Goroutine leaks

curl -s 'http://127.0.0.1:6060/debug/pprof/goroutine?debug=1' | head -60   # grouped counts

Thousands of goroutines parked at the same line (chan receive, select, IO wait) are the leak. Typical causes: missing ctx.Done() case, unbuffered channel send with no reader after a timeout, HTTP response bodies not closed. In tests, go.uber.org/goleak catches leaks automatically.

Runtime knobs (after code fixes, not instead)

  • GOMEMLIMIT (soft memory limit, e.g. 90% of container memory) lets the GC work harder near the limit instead of OOMing; pair with sensible GOGC.
  • Go 1.25+ sets GOMAXPROCS from the container CPU limit; on older versions use go.uber.org/automaxprocs to avoid throttling.
  • PGO: save a representative production CPU profile as default.pgo in the main package directory; go build uses it automatically (Go 1.21+). Typical gain is a few percent of CPU; refresh it periodically.
  • Go 1.25 adds runtime/trace.FlightRecorder for capturing the last few seconds of trace around a rare slow request.

Gotchas

  • A CPU profile only shows on-CPU time. A handler waiting on the database is invisible there; use block profiles or tracing.
  • inuse_space shows what is live now; alloc_space shows churn since start. Leaks are inuse; GC pressure is alloc.
  • Profiles from -race or debug builds (-gcflags=all=-N -l) are not representative.
  • Heap profiles are sampled (every 512 KB by default); small one-off allocations can be missed, totals are estimates.

Report template

## pprof: <service> (Go <ver>, <load>)
Profile: CPU 30 s at 2k rps; allocs during same window
| Function | flat | cum | Finding |
|---|---|---|---|
| orders.BuildReport | 4% | 38% | json.Marshal per row inside loop |
| runtime.mallocgc | 17% | 22% | allocation churn from the same loop |
Fix: encode once with json.Encoder over a preallocated slice
benchstat: BuildReport -61% time/op, -83% allocs/op (n=10, p=0.000)
Load test: p99 210 ms -> 88 ms, CPU 3.1 -> 1.4 cores

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Go pprof Profiling"
  short_description: "Right profile, right line, benchstat proof"
  default_prompt: "$go-pprof Our Go API uses 4 cores at 2k rps, profile it and cut CPU"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem