gcscope — Go Garbage Collector Visualizer


Read this in other languages: Russian
gcscope is a terminal UI (TUI) and CLI for visualizing Go GC behavior in real time from gctrace, gcpacertrace, and runtime/metrics: GC cycles, stop-the-world (STW) pauses, heap live/goal dynamics, and GC pacer signals.
It is designed for fast feedback during performance work:
- spot problematic STW spikes (p99/max) under load
- see how GC frequency changes across runs
- see heap live approach heap goal as pacing becomes more aggressive
- compare two runs using snapshots (
diff)
Contents
How It Works
gcscope has two data sources:
run (primary): starts your binary, ensures GODEBUG contains gctrace=1,gcpacertrace=1, parses the target stderr.
attach (secondary): polls an HTTP endpoint that exports runtime/metrics in a gcscope-friendly JSON format (via pkg/reporter).
No code changes are required for run. For attach, you add a small HTTP endpoint to the target service.
Install
Pick one of the options below.
Install (Go)
Install the gcscope CLI into your GOBIN:
go install github.com/timur-developer/gcscope/cmd/gcscope@latest
After that you can use gcscope as a normal CLI (from any directory):
gcscope lab churn
gcscope run ./path/to/your-binary -- --your-flag value
gcscope attach http://127.0.0.1:8080/gcscope/metrics
gcscope diff ./a.json ./b.json
Built-in help:
gcscope --help
gcscope run --help
Install (Prebuilt binaries)
Download a prebuilt binary from GitHub Releases (Assets), pick the archive for your OS/arch, then extract it.
Run from the extracted folder:
Windows (PowerShell):
.\gcscope.exe lab churn
macOS / Linux:
chmod +x ./gcscope
./gcscope lab churn
To run gcscope from any directory, move it to a folder in your PATH (or add the extracted folder to PATH).
Run From Source (No Install)
From source:
go run ./cmd/gcscope lab churn
Quickstart (1 minute)

Prerequisites: Go 1.22+ and a reasonably large terminal window.
1) Try the built-in demo workload
From source (no install):
go run ./cmd/gcscope lab churn
Or via Makefile:
make lab-churn
Open in-app help any time: ? / h / f1.
2) Run on your binary (step-by-step)
- Build your target app as a binary:
go build -o ./myapp ./cmd/myapp
- Run it under observation (note the
-- separator for passing args to your program):
go run ./cmd/gcscope run ./myapp -- --your-flag value
- In the UI:
- press
? to see all hotkeys
- press
space to pause/resume
- when paused, use
left/right (and home/end) to scrub history
- press
s to write a snapshot to tmp/snapshots (by default)
If you want to use gcscope as a CLI, install it once and run gcscope ... directly (see Install).
Usage
run (run your binary under observation)
Run your Go program under observation:
gcscope run ./path/to/your-binary
This mode is intended for checking how Go's garbage collector behaves in your project during a real run.
run works with a compiled binary (not a .go file), so build your program first, then pass the resulting executable path to gcscope.
Need flags or want to pass args/flags to your program? See Configuration.
From source (no install):
go run ./cmd/gcscope run ./path/to/your-binary
Makefile shortcut:
make run TARGET=./path/to/your-binary
lab (built-in demo workloads)
Built-in demo presets:
gcscope lab alloc
gcscope lab churn
gcscope lab idle
gcscope lab spike
What the presets mean (synthetic workloads):
alloc: steady small/medium allocations with some retention; heap live gradually grows and GC cadence stays relatively stable
churn: repeated large bursts with short retention; useful for stressing STW and the pacer
idle: mostly idle with occasional bursts; useful for observing low-frequency GC behavior
spike: light background traffic with periodic heavy bursts; makes heap and STW spikes easy to see
attach (connect to a runtime/metrics HTTP endpoint)
Attach to a running service that exposes runtime/metrics in gcscope JSON format.
- Add
pkg/reporter to your service:
package main
import (
"log"
"net/http"
"github.com/timur-developer/gcscope/pkg/reporter"
)
func main() {
rep := reporter.New()
mux := http.NewServeMux()
mux.Handle(rep.Path(), rep.Handler())
log.Fatal(http.ListenAndServe(":8080", mux))
}
- Attach:
gcscope attach http://127.0.0.1:8080/gcscope/metrics
Notes (attach mode):
- the endpoint payload is based on
runtime/metrics, so values differ from run mode
- the target process environment (
GOGC, GOMEMLIMIT, GODEBUG) is not available; UI shows n/a
diff (compare two snapshot files)
Compare two snapshots:
gcscope diff ./a.json ./b.json
What diff prints:
- a short summary for snapshot A and B (
gc_cycles_total, heap_live_mb, stw_p50/p99/max_us)
- delta (B-A) for
heap_live_mb and STW window stats
What You See (Metrics & Panels)

gcscope keeps a sliding window of recent GC events (--window-size, default: 200) and shows both raw per-cycle values and derived stats over that window.
Current Values
GC cycles total: current GC cycle number
last STW (us): last cycle STW pause (sweep term + mark term, converted to microseconds)
heap live (MB) / heap goal (MB): live heap and current goal
heap: live/goal: a compact live-vs-goal indicator
max STW (us): max STW over the visible window
gc: rate as GCs/min and/or average GC interval
stw: bad STW count/percent (based on thresholds) and forced GC count
time since last GC, uptime
stw thresholds: warn / bad (see Configuration)
- snapshot status and snapshot directory
- env context (
GOGC, GOMEMLIMIT, GODEBUG) in run/lab (not available in attach)
Charts
- Heap live over time (MB): time-series of heap live
- STW p50/p99/max over time (us): time-series derived from the sliding window
- STW per cycle: per-cycle bar chart; labels can show STW or heap live (toggle with
l)
Cycle Details (selected GC event)
For the currently selected cycle (live cursor, or scrubbed history when paused):
- GC #, time since start, forced
- STW total (us) + breakdown: sweep term / mark term
- heap (MB): start/end and live/goal
- gc cpu (%)
- pacer signals (when available): assist ratio, assist workers, pages swept
Controls

Full list of hotkeys is always available in the in-app Help (? / h / f1).
Core:
? / h / f1 toggle Help
q / ctrl+c quit
space pause/resume live updates
left / right scrub history when paused
home / end jump to first/last event when paused
s write a snapshot
Layout and labels:
g toggle layout (spaced/tight)
l toggle STW bar labels mode (GC+STW -> GC+Heap -> GC-only)
Charts:
z switch focused chart (Heap/STW). Zoom/pan applies to the focused chart.
+ / - zoom Y for focused chart
0 reset Y zoom/pan for focused chart
shift+up / shift+down pan Y for focused chart
[ / ] zoom time span (X axis): all -> 1h -> 15m -> 5m -> 1m (and back)
r reset focus, zoom/pan, and time span
Configuration
Global flags (and their env overrides):
--window-size (GCSCOPE_WINDOW_SIZE) samples kept in memory (default: 200)
--snapshot-path (GCSCOPE_SNAPSHOT_PATH) snapshot directory (default: tmp/snapshots)
--exit-snapshot (GCSCOPE_EXIT_SNAPSHOT) write a snapshot on exit (default: true)
--no-alt-screen (GCSCOPE_NO_ALT_SCREEN) disable alt screen buffer
--stw-warn-us (GCSCOPE_STW_WARN_US) STW warning threshold (default: 200)
--stw-bad-us (GCSCOPE_STW_BAD_US) STW bad threshold (default: 1000)
Mode-specific env vars:
GCSCOPE_RUN_TARGET
GCSCOPE_ATTACH_URL, GCSCOPE_POLL_INTERVAL
GCSCOPE_LAB_PRESET
GCSCOPE_DIFF_A, GCSCOPE_DIFF_B
All flags can be provided via their GCSCOPE_* env equivalents listed above.
Flags & Argument Passing
Global flags (like --window-size, --stw-bad-us) go before the subcommand because they apply to all modes.
In run mode, -- separates gcscope arguments from the target program arguments. Everything after -- is passed to your binary unchanged.
Template:
gcscope [global flags] run <target-binary> -- [target args...]
Example:
gcscope --window-size 500 --stw-bad-us 2000 run ./path/to/your-binary -- --your-flag value
Snapshots
- Default directory:
tmp/snapshots
- Manual snapshot: press
s
- Exit snapshot: enabled by default; skipped if a manual snapshot was created recently
What a snapshot contains:
- current values (
gc_cycles_total, last_stw_us, heap_live_mb, heap_goal_mb)
- window stats (
stw_p50_us, stw_p99_us, stw_max_us)
- the list of recent GC events (the same window used by the UI), including parsed pacer fields when available
Snapshots are plain JSON files. They are useful for sharing, tracking regressions, and comparing two runs with gcscope diff.
Makefile Commands
make help prints all commands. Most common ones:
make ci: run lint + tests + build
make lint: run golangci-lint
make test: run go test ./...
make build: run go build ./... (sanity check)
make install: install gcscope into your Go bin directory
make lab (or make lab-churn, etc.): run demo workloads
make run TARGET=... ARGS="-- ...": run your binary under observation
make attach URL=...: attach to a running service (default URL is http://127.0.0.1:8080/gcscope/metrics)
make diff A=... B=...: compare two snapshot files
Maintainers:
make testbin: rebuild embedded lab binaries for all supported OS/arch
make release-snapshot: local GoReleaser build (--snapshot --clean)
Notes / FAQ
attach mode cannot see the target process env (GOGC, GOMEMLIMIT, GODEBUG), so UI shows n/a for these fields.
- If you see no updates, your program might simply not be hitting GC cycles yet (try a workload that allocates more, or use
lab churn).
- If your terminal behaves oddly, try
--no-alt-screen (or GCSCOPE_NO_ALT_SCREEN=true).
- Very small STW values may display as 0 due to
gctrace formatting.
Development
make ci
make lint
make test
make build
Maintainers:
make testbin
make release-snapshot
License
MIT. See LICENSE.