skip to content

What does running a Go binary with GODEBUG=gctrace=1 print, and where does that output go?

level: juniorimportance: nice to knowfreq 28%

answer

  1. no code change, no rebuild
  2. read from the environment at start-up
  3. one line per completed collection
  4. goes to stderr, not stdout

basics

~20 s

GODEBUG=gctrace=1 makes the Go runtime write one summary line to standard error after every completed garbage-collection cycle. No rebuild and no code change are needed; the runtime reads the variable from the environment when the process starts.

solid answer

~50 s

`GODEBUG` is an environment variable the Go runtime parses at start-up, and `gctrace=1` is one of its settings. With it on, the runtime emits a single line to **standard error** every time a garbage-collection cycle finishes. Each line carries the cycle number, the seconds since the program started, the cumulative share of CPU time spent collecting, the phase timings, the heap size before and after the cycle plus the live heap that survived, and the number of Ps in use. A cycle triggered explicitly by `runtime.GC` is marked `(forced)` at the end of its line. Nothing about the binary changes — you set the variable and restart the process, which is why this is usually the first thing you turn on when a service's memory behaviour looks wrong and you cannot ship a new build.

code

text · 4 lines
text
$ GODEBUG=gctrace=1 ./collector 2>gc.log
$ head -2 gc.log
gc 1 @0.021s 0%: 0.019+0.55+0.003 ms clock, 0.15+0.12/0.31/0.28+0.02 ms cpu, 4->4->1 MB, 5 MB goal, 0 MB stacks, 0 MB globals, 8 P
gc 2 @0.104s 0%: 0.011+0.71+0.004 ms clock, 0.09+0.05/0.42/0.11+0.03 ms cpu, 5->5->2 MB, 5 MB goal, 0 MB stacks, 0 MB globals, 8 P

go deeper

for a junior

Be ready to say that it is an environment variable, that it needs no rebuild or import, and that it produces one line per completed collection on standard error. Knowing how to capture it with 2> is enough at this level.

for a middle

You are expected to read the line, not just produce it: name the fields, and explain that the cycle number and timestamp let you see how often collections happen. Mention the (forced) suffix and what it implies.

for a senior

Show that you reach for it because it needs no deploy, and that you read a series rather than a line. Say plainly what it cannot tell you — allocation sites — so nobody mistakes it for a profile.

for a principal

Frame it as the cheapest observability you get for free on any built binary, and set the expectation that it is turned on for a bounded window with the log volume of a hot service in mind, not left on everywhere by default.

## What `GODEBUG` is `GODEBUG` is an ordinary environment variable that the Go runtime reads once, at process start-up, before your `main` runs. Its value is a comma-separated list of `name=value` settings that switch on runtime instrumentation or change runtime behaviour. `gctrace=1` is one of those settings. Because it is read from the environment, it applies to any Go binary that is already built — you do not recompile, you do not import anything, and you do not add a line of code. That property is the whole reason this is the first diagnostic reached for in production: you can set it on a container, restart the process, and see collector behaviour within seconds. ## What it emits With `gctrace=1` set, the runtime writes exactly one line to **standard error** each time a garbage-collection cycle completes. Nothing is written per allocation, per second, or per object — the unit is the completed cycle. A short program that collects rarely produces a handful of lines; a program allocating hard can produce hundreds per second, which is worth knowing before you turn it on in a hot service. A line looks roughly like this: ``` gc 47 @12.043s 3%: 0.11+5.8+0.09 ms clock, 0.9+1.4/4.2/0+0.7 ms cpu, 42->44->21 MB, 43 MB goal, 1 MB stacks, 0 MB globals, 8 P ``` Reading left to right: `gc 47` is the cycle number, counting from the start of the process. `@12.043s` is how long the program had been running when the cycle finished. `3%` is the share of the program's total CPU time spent in garbage collection since start-up. The `ms clock` group is the wall-clock time of the cycle's phases, and the `ms cpu` group is the CPU time of the same work summed across processors. The `42->44->21 MB` triple is the heap size when the cycle started, the heap size when it ended, and the live heap that survived marking. `43 MB goal` is the heap size the collector was aiming to finish under. The `stacks` and `globals` figures report how much scannable goroutine-stack and global-variable memory there was. `8 P` is the number of logical processors the runtime was using. If a cycle was triggered by an explicit `runtime.GC()` call rather than by allocation, the line ends with `(forced)`. That single word is often the answer to "why is this service collecting so often" — some library or start-up path is forcing collections. ## Standard error, not standard output The runtime writes these lines directly to file descriptor 2. It does not go through the `log` package, your logger, or any writer you configured, so redirecting your program's own logging does not capture or suppress it. Standard error is the right channel: your program's real output on standard output — JSON, CSV, a rendered report, data piped into another tool — stays uncorrupted, while log collectors that capture both streams still pick the trace up. In practice you run something like `GODEBUG=gctrace=1 ./collector 2>gc.log` and analyse `gc.log` separately. ## What it costs, and what it is not The collector does the same work either way; the only added cost is formatting and writing a short line per cycle. On a service that collects a few times a second that is negligible. On one that collects hundreds of times a second the log volume itself becomes the problem, so it is normal to enable it for a bounded window rather than permanently. It is important to be clear about what a gctrace line is *not*. It is a summary of collector activity, not a profile: it tells you how much memory survived a cycle, but never *which* code allocated it or *which* objects are being retained. Finding allocation sites is a separate exercise with separate tools. gctrace answers questions of shape and trend — is the live heap growing, are cycles getting more frequent, is collection eating a rising share of CPU — and it answers them without changing the program. ## How it is typically used The useful reading is almost never a single line. You capture a stretch of lines and look at how the numbers move: a live heap that climbs cycle after cycle, a cumulative CPU percentage that keeps rising, cycles crowding closer together in the `@…s` timestamps. One line on its own tells you the state of one moment; the series tells you the story.

  • Why does the runtime write gctrace lines to standard error rather than standard output?
    Standard output belongs to the program: it may be piped into another tool or carry structured data, and interleaving runtime diagnostics would corrupt it. Standard error is the conventional channel for out-of-band information, is still captured by container log collectors, and can be redirected separately with `2>`. The runtime writes it directly to file descriptor 2, bypassing whatever logger the program configured.
  • How can you tell from a gctrace line that the cycle was triggered by an explicit runtime.GC() call?
    The line ends with `(forced)`. Cycles started by the pacer as the heap grows carry no such marker. Seeing many forced cycles usually means some start-up path or library is calling `runtime.GC()` deliberately, and that is worth chasing down because forced collections ignore the normal heap trigger and can cost far more CPU than the code that requests them expects.
  • What does leaving GODEBUG=gctrace=1 on in production actually cost?
    The collector itself does no extra work; the cost is formatting and writing one short line per completed cycle to standard error. That is negligible for a service collecting a few times a second, but a heavily allocating service can collect hundreds of times a second, and then the log volume and the write syscalls become the real expense. Enable it for a bounded window rather than permanently.

saying these in an interview costs you the question

  • Says the binary must be rebuilt with a special flag first
  • Expects the lines on standard output and loses them when piping
  • Thinks gctrace reports which code allocated the memory
  • Believes gctrace changes how the collector schedules its work
  • Reads a single line as the process's total memory usage