Go perf godebug and metrics
Skill ctoth/golang-skills-plugin/plugins/performant-golang/skills/go-perf-godebug-and-metrics
Agent skill plugins for Go code quality and performance work.
npx -y skills add ctoth/golang-skills-plugin --skill go-perf-godebug-and-metricsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Guides Go runtime observability — the GODEBUG trace knobs (gctrace, schedtrace/scheddetail, inittrace, allocfreetrace, madvdontneed) and reading a gctrace line, the low-overhead runtime/metrics package and its histogram metrics, preferred over runtime.ReadMemStats which stops the world, and exposing metrics via expvar/Prometheus. Fires on "GODEBUG", "gctrace", "read runtime metrics", "monitor GC in production", "ReadMemStats vs runtime/metrics". Routes GC to go-perf-gc-tuning, scheduler to go-perf-goroutines-scheduler.
SKILL.md
14.9 KB, ~3.9k tokens by cl100k_base, as published. Nobody here has run it
Go GODEBUG & runtime/metrics
This skill owns runtime observability for performance: the GODEBUG knobs that make the runtime narrate what it is doing, and the runtime/metrics package that lets a process read its own runtime stats cheaply. It is a reference skill — it tells you what to look at; the action you take routes to a sibling (go-perf-gc-tuning for GC knobs, go-perf-goroutines-scheduler for scheduler depth, go-perf-tail-latency for the histograms, go-perf-execution-tracer for the tracer, go-perf-os-tooling for OS profilers).
All Go below builds clean under gofmt, go vet, and go test on Go 1.25 / 1.26.
1. GODEBUG — Two Jobs in One Variable
GODEBUG is a comma-separated list of key=value settings that "controls the execution of certain parts of a Go program" (godebug). It carries two unrelated kinds of setting, and conflating them is a common error:
- Compatibility toggles — Go's backwards-compatibility escape hatch. When fixing a bug would break code that "depends on buggy (including insecure) behavior," GODEBUG lets old code keep the old behavior (godebug). These have defaults derived from the
goline ingo.mod, agodebugdirective, or a//go:debugline — e.g.panicnil=1,http2client=0, and the perf-relevantcontainermaxprocs({Name: "containermaxprocs", Package: "runtime", Changed: 25, Old: "0"}— toggles the Go 1.25 container-awareGOMAXPROCS) (godebugs/table.go). Each compatibility setting also exports a counter/godebug/non-default-behavior/<name>:eventsso you can see in production whether old behavior is firing (godebug; metrics/description.go). - Runtime trace knobs (§2) — diagnostic output to stderr. These are not compatibility settings; they are debugging instrumentation set only in the environment (
GODEBUG=gctrace=1 ./server). "Unrecognized settings in theGODEBUGenvironment variable are ignored" (godebug) — so a typo silently does nothing.
2. The Trace Knobs (set in the environment, read stderr)
Each knob below is documented in runtime's package doc (runtime: Environment Variables, mirrored in src/runtime/extern.go). They emit lines to standard error; the formats are "subject to change."
GODEBUG= | What it emits | Use it to see |
|---|---|---|
gctrace=1 | one line per GC (§3) | GC frequency, pause, heap sizes, GC CPU% |
schedtrace=N | one line every N ms summarizing scheduler state | run-queue depth, idle Ps/Ms |
schedtrace=N,scheddetail=1 | detailed multiline per-P/M/G state every N ms | which goroutines are runnable/blocked |
inittrace=1 | one line per package with init work (§3) | slow / allocation-heavy package init |
scavtrace=1 | one line ~per GC cycle from the scavenger | memory returned to the OS, RSS behavior |
madvdontneed=0 | (behavior toggle) use MADV_FREE not MADV_DONTNEED on Linux | why RSS stays high until memory pressure |
Two cautions:
schedtrace/scheddetailandgctraceare diagnostics, not always-on.scheddetail=1is verbose; combining several traces interleaves their lines on the same stderr and makes both unreadable. Set one at a time.allocfreetraceis gone. It historically emitted a stack trace on every allocation and free — astronomically heavy and only ever a last resort. It has been removed from the modern runtime (no longer present insrc/runtime/extern.goorsrc/runtime/*.goas of the Go 1.26 tree). For per-allocation insight today, use a heap profile (go-perf-pprof-profiling) or the execution tracer (go-perf-execution-tracer), not a GODEBUG knob.
3. Reading a gctrace Line Field-by-Field
gctrace=1 "causes the garbage collector to emit a single line to standard error at each collection, summarizing the amount of memory collected and the length of the pause" (runtime). The format (verbatim, "subject to change"):
gc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB goal, # MB stacks, #MB globals, # P
Field by field, with the runtime/metrics metric that supersedes it (the doc lists these mappings inline):
| Field | Meaning | Modern metric |
|---|---|---|
gc # | the GC number, incremented each GC | — |
@#s | seconds since program start | — |
#% | percentage of time spent in GC since program start | /cpu/classes/gc/total:cpu-seconds |
#+#+# ms clock | wall-clock time for the GC phases | — |
#+#/#/#+# ms cpu | CPU time across the phases | /cpu/classes/gc/* |
#->#-># MB | heap size at GC start, at GC end, and live heap | /gc/scan/heap:bytes |
# MB goal | goal heap size | /gc/heap/goal:bytes |
# MB stacks | estimated scannable stack size | /gc/scan/stack:bytes |
#MB globals | scannable global size | /gc/scan/globals:bytes |
# P | number of processors used | /sched/gomaxprocs:threads |
"The phases are stop-the-world (STW) sweep termination, concurrent mark and scan, and STW mark termination. The CPU times for mark/scan are broken down in to assist time (GC performed in line with allocation), background GC time, and idle GC time. If the line ends with (forced), this GC was forced by a runtime.GC() call" (runtime). The #% GC-CPU field and the #->#-># live-heap field are the two to read first: a rising #% means GC is eating throughput, and a live heap that climbs run-to-run means a leak or a GOMEMLIMIT worth setting (act → go-perf-gc-tuning).
inittrace=1 is read the same way — init # @#ms, # ms clock, # bytes, # allocs per package, surfacing a package whose init is slow or allocates heavily (runtime).
4. runtime/metrics — The Modern, Low-Overhead Way
For anything programmatic (dashboards, alerts, in-process budgets), read runtime/metrics, not runtime.ReadMemStats. The package "provides a stable interface to access implementation-defined metrics exported by the Go runtime … similar to existing functions like runtime.ReadMemStats … but significantly more general" (runtime/metrics). Metrics are keyed by string names with an embedded unit (/path:unit), discovered at runtime via metrics.All(), so the set grows across Go versions without breaking your code; the Kind of a given metric is guaranteed stable.
package main
import (
"fmt"
"runtime/metrics"
)
// sampleGC reads three runtime metrics in one call. Reuse the []metrics.Sample
// slice across calls; metrics.Read fills in each Value in place.
func sampleGC() {
samples := []metrics.Sample{
{Name: "/gc/heap/allocs:bytes"}, // cumulative bytes allocated
{Name: "/sched/goroutines:goroutines"}, // live goroutines
{Name: "/sched/latencies:seconds"}, // runnable→running wait (histogram)
}
metrics.Read(samples)
for _, s := range samples {
switch s.Value.Kind() {
case metrics.KindUint64:
fmt.Printf("%s = %d\n", s.Name, s.Value.Uint64())
case metrics.KindFloat64:
fmt.Printf("%s = %f\n", s.Name, s.Value.Float64())
case metrics.KindFloat64Histogram:
h := s.Value.Float64Histogram()
fmt.Printf("%s = histogram with %d buckets\n", s.Name, len(h.Counts))
case metrics.KindBad:
panic("unsupported metric " + s.Name) // name not in metrics.All()
}
}
}
Key API (runtime/metrics):
metrics.Read([]Sample)fills eachSample.Valuein place; reuse the slice between calls — the read is cheap and does not stop the world.metrics.All() []Descriptionlists every supported metric (Name,Description,Kind,Cumulative). Iterate it rather than hardcoding names, so you skip aKindBadon an older runtime.- Histograms (
KindFloat64Histogram) carryBuckets []float64andCounts []uint64— the raw material for a p99 without coordinated omission (methodology →go-perf-tail-latency).
A few metric names worth knowing (from metrics/description.go)
| Metric | Kind | Reads |
|---|---|---|
/gc/heap/allocs:bytes | uint64, cumulative | total bytes ever allocated (allocation rate = its slope) |
/gc/heap/allocs:objects | uint64, cumulative | total heap objects allocated |
/gc/heap/live:bytes | uint64 | live bytes marked by the previous GC |
/gc/gogc:percent, /gc/gomemlimit:bytes | uint64 | the effective GOGC / GOMEMLIMIT |
/sched/latencies:seconds | histogram | goroutine runnable→running wait (scheduler saturation) |
/sched/goroutines:goroutines | uint64 | live goroutines (leak detection) |
/sync/mutex/wait/total:seconds | float64, cumulative | global lock-contention trend |
/memory/classes/total:bytes | uint64 | all memory mapped read-write by the runtime |
Deprecation to know: /gc/pauses:seconds is now documented as "Deprecated. Prefer the identical /sched/pauses/total/gc:seconds." (metrics/description.go). The newer GC STW pause histograms are /sched/pauses/total/gc:seconds (full pause) and /sched/pauses/stopping/gc:seconds (just the time to stop all Ps); non-GC STW events live under /sched/pauses/*/other:seconds. Read code emitting /gc/pauses:seconds and migrate it.
5. Why runtime/metrics Beats runtime.ReadMemStats
runtime.ReadMemStats(m *MemStats) stops the world to take its snapshot — its implementation begins stw := stopTheWorld(stwReadMemStats) (src/runtime/mstats.go). Calling it on a timer in a hot service injects a global pause on every scrape, and it returns one coarse, fixed MemStats struct that cannot express histograms or grow with the runtime. runtime/metrics is the general, forward-compatible, non-STW replacement (§4). Prefer it for all new code; reach for ReadMemStats only when interfacing with old code that already depends on a specific MemStats field.
6. Exposing Metrics Continuously
A one-shot metrics.Read answers "now"; production wants a time series.
expvaris the zero-dependency option. It "provides a standardized interface to public variables … exposed via HTTP at/debug/varsin JSON format," and importing it for its side effect registers a handler pluscmdlineandmemstatsautomatically (expvar). Publish your own withexpvar.NewInt("requests")orexpvar.Publish(name, v)(vis anyVarwhoseString()returns valid JSON). Bridgeruntime/metricsinto it with anexpvar.Func:
import (
"expvar"
"runtime/metrics"
)
func init() {
const goroutines = "/sched/goroutines:goroutines"
expvar.Publish("go_goroutines", expvar.Func(func() any {
s := []metrics.Sample{{Name: goroutines}}
metrics.Read(s)
return s[0].Value.Uint64()
}))
}
- Prometheus is the standard for real fleets: the official client's
collectors.NewGoCollectoralready sources fromruntime/metrics, so you get/gc/*and/sched/*on a scrape endpoint with no custom wiring. (Use it over hand-rolled gauges; correctness of the client API is out of scope here — this skill's claim is only which source to pull from:runtime/metrics, notReadMemStats.)
Note expvar's default /debug/vars handler — like net/http/pprof — should not be on a public port; gate it to an internal interface.
7. Routing to Related Skills
This plugin (go-perf-*):
go-perf-gc-tuning— acting on a badgctraceline or/gc/*metric:GOGC,GOMEMLIMIT, the pacer, allocation rate as the primary lever.go-perf-goroutines-scheduler— interpretingschedtrace//sched/*depth: GMP model, work-stealing, container-awareGOMAXPROCS.go-perf-tail-latency— turning/sched/latencies:secondsand/sched/pauses/total/gc:secondshistograms into p99/p999 without coordinated omission.go-perf-execution-tracer— when you need a timeline, not a counter:runtime/trace,go tool trace, the flight recorder.go-perf-os-tooling— OS-level profilers (perf, flame graphs) when the cost is below the runtime.go-perf-methodology— the decision layer: which diagnostic answers which question.
Parent / sibling marketplace (go-*):
go-slog-logging— structured logging, distinct from metric counters.go-version-feature-map— version floors: container-awareGOMAXPROCS(1.25), Green Tea GC default (1.26).
8. Don't
- Don't poll
runtime.ReadMemStatson a timer — it stops the world every call (mstats.go); useruntime/metrics(§5). - Don't hardcode
/gc/pauses:seconds— it is deprecated in favor of the identical/sched/pauses/total/gc:seconds(description.go). - Don't reach for
allocfreetrace— it was removed; use a heap profile or the tracer (§2). - Don't leave
scheddetail=1(or several traces) running together — verbose, interleaved stderr that drowns the signal (§2). - Don't typo a GODEBUG key — unrecognized settings are silently ignored, so the trace just never appears (godebug).
- Don't confuse a GODEBUG compatibility toggle with a trace knob — the first has go.mod-derived defaults and a
/godebug/non-default-behavior/*counter; the second is environment-only stderr instrumentation (§1). - Don't expose
/debug/vars(or pprof) on a public port — gate it internally (§6). - Don't read
runtime/metricsand then guess at the fix here — act ingo-perf-gc-tuning/go-perf-goroutines-scheduler; this skill only tells you what to look at.
9. Reference Files
Wrong/right anti-patterns in LLM-generated runtime-observability code, with citations:
${CLAUDE_SKILL_DIR}/references/common-mistakes.md
Source provenance for every claim in this skill:
${CLAUDE_SKILL_DIR}/references/sources.yaml
Gives 0 of the 12 instructions most monitoring observability skills give in ~3.9k tokens
Counted across 481 of the 483 authors here whose files we hold, read 2026-08-06
- link every alert to a runbookin 43 of 481, across 35 files
- use structured json loggingin 36 of 481, across 31 files
- alert on user-facing symptomsin 20 of 481, across 15 files
- emit structured JSON logs with stable event namesin 18 of 481, across 13 files
- propagate trace context across boundariesin 16 of 481
- use histograms for latency trackingin 14 of 481, across 9 files
- use OpenTelemetry for distributed tracingin 13 of 481, across 8 files
- include a correlation ID on every log linein 13 of 481, across 8 files
- Define service level objectivesin 10 of 481, across 7 files
- Call useAzureMonitor before importing other modulesin 9 of 481, across 2 files
- stop and ask for clarification if inputs are missingin 9 of 481, across 2 files
- define on-call questions before adding telemetryin 9 of 481, across 4 files
Said here and by no other author read
- set one GODEBUG trace knob at a time
- read the GC-CPU and live-heap fields first
- use runtime/metrics for programmatic observability
- reuse the metrics sample slice across reads
- iterate metrics.All instead of hardcoding names
- migrate code off the deprecated gc/pauses metric
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.