Watching the Go garbage collector stop
I noticed a small server pausing at intervals under an allocation-heavy test. The average request time looked respectable, but individual requests occasionally took a visible detour. That pattern made the old garbage collector worth examining.
I reduced the trigger to a loop that replaces short-lived buffers:
for i := 0; i < n; i++ {
b := make([]byte, 32*1024)
consume(b)
}
The first surprise was that allocation speed and collection latency are separate observations. Allocation can be cheap while creating enough work for periodic collections. An average over the entire run smooths those pauses into a harmless-looking number.
The collector in this runtime is a stop-the-world collector. When a collection begins, Go execution is brought to a safe point and paused while the runtime determines which heap objects are reachable. It starts from roots such as stacks and global data, follows pointers through reachable objects, and eventually reclaims storage that was not found. Application goroutines do not continue doing ordinary work during that collection.
This explains why adding processors does not simply make the pause disappear. It may improve the application between collections, but the collector’s coordination and tracing remain a shared event. It also explains why retaining one pointer to a large structure matters: reachability, not whether I personally remember using the object, determines whether it survives.
I prefer reducing needless allocation only after a profile or latency measurement identifies it. Reusing a buffer at a clear ownership boundary can help. Converting every local variable into a global cache can help the heap grow mysterious hair. Smaller live data and fewer transient objects are good outcomes; cleverness measured only by line count is not.
The caveat is that stack and heap placement are compiler decisions, and the compiler is changing. I avoid writing code that depends on a guessed escape decision. I also avoid claims about a future collector when explaining this one. Today’s pauses are real even if the runtime will improve.
For the server, pooling two large scratch buffers around a serialized operation removed most of the allocation burst. I kept the simpler allocating version in a benchmark so the trade remains measurable. The garbage collector was doing exactly what it had been asked to do; I had simply been sending rather enthusiastic invitations.