skip to content

What did Go 1.26's Green Tea garbage collector change, and what stayed the same?

level: seniorimportance: nice to knowfreq 22%

answer

  1. the marker was waiting on memory, not CPU
  2. batch the work by span, not by object
  3. a build-time experiment flag, not a runtime one
  4. default since Go 1.26, opt out to compare
  5. same tri-colour, same non-moving design

basics

~20 s

Green Tea, on by default since Go 1.26, changes how the marker scans memory: span by span for better locality instead of chasing objects one pointer hop at a time. The collector stays concurrent, tri-colour, non-generational and non-moving.

solid answer

~50 s

Green Tea is a new **marking algorithm**, not a new collector. Classic tri-colour marking scans object by object, and each pointer hop is a likely cache miss, so a big pointer-dense heap makes the marker memory-latency-bound rather than CPU-bound. Green Tea batches work by span so objects that live near each other are scanned together, which gives the hardware something sequential and prefetchable to chew on. It appeared as `GOEXPERIMENT=greenteagc` in Go 1.25 and became the default in Go 1.26, where you can opt out with `GOEXPERIMENT=nogreenteagc`; the reported win is roughly 10–40% less GC overhead, and it is very workload-dependent. What did not change: the collector is still concurrent tri-colour mark with lazy sweep, still non-generational, still non-moving, and `GOGC`/`GOMEMLIMIT` mean exactly what they did before. If you care, A/B the two builds under your own load rather than trusting the headline range.

code

text · 5 lines
text
# Go 1.26 default build: Green Tea marking
go build -o svc-greentea ./cmd/svc

# Same commit, previous marking algorithm
GOEXPERIMENT=nogreenteagc go build -o svc-classic ./cmd/svc

go deeper

for a junior

You are not expected to know Green Tea by name. Knowing that Go's collector is concurrent mark-and-sweep and that recent releases improved its efficiency without changing how you write code is plenty here.

for a middle

Be able to say what kind of change it is: the marker scans memory span by span for locality, it became the default in Go 1.26, and none of the collector's guarantees or knobs changed.

for a senior

Show how you would verify a runtime improvement rather than assume it: two builds of the same commit, representative load, GC CPU share and tail latency compared, and a clear reading of a null result.

for a principal

Own the upgrade posture: decide when the fleet moves to a toolchain whose default collector behaviour changed, what evidence gates it, and make clear that runtime improvements do not substitute for cutting allocation rate and live-set size.

## What problem Green Tea solves A tracing collector's inner loop is: take a found object, read its pointer fields, follow each one to another object. On a large heap those targets are scattered, so nearly every hop is a cache miss. The marker then spends most of its time *waiting on memory*, not computing. Adding CPU to that does not help much — the machine is stalled, not busy. That is the bottleneck Green Tea attacks. Rather than treating the unit of marking work as a single object, it organises work around **spans** — the runs of pages the Go heap is built from, each holding many same-size-class objects. Objects found in the same span are scanned together, so the marker touches memory in a much more sequential, prefetch-friendly pattern instead of jumping across the heap one pointer at a time. The payoff is a locality win, and the Go team reported it as roughly **10–40% less garbage-collection overhead** depending on workload. Workloads with big, pointer-dense live heaps — graphs, trees, maps of pointers, long-lived caches — see the most. A service that barely collects sees essentially nothing. ## Timeline and how to turn it on or off - **Go 1.25**: available as an opt-in experiment, `GOEXPERIMENT=greenteagc`. - **Go 1.26**: on by default; opt out with `GOEXPERIMENT=nogreenteagc`. `GOEXPERIMENT` is a **build-time** setting, not a runtime knob: it selects a runtime variant when the binary is compiled, so flipping it means rebuilding, not restarting with a different environment variable. ## What Green Tea explicitly does not change This is the part interviewers are usually probing, because it separates "read the release notes" from "understands the collector": - **Still concurrent tri-colour marking.** The white/grey/black model and the fact that marking runs alongside your goroutines are unchanged. - **Still lazy sweep.** Marking decides what is live; spans are swept afterwards, largely as goroutines allocate. - **Still non-generational.** There is no nursery, no promotion, no object age. Every cycle traces the whole live set. - **Still non-moving.** No compaction, no relocation; heap object addresses remain stable. - **Same knobs and same semantics.** `GOGC` is still a heap-growth percentage, `GOMEMLIMIT` is still a soft ceiling, and the collector's CPU budget policy is unchanged. - **Same programming model.** Nothing about `unsafe`, cgo pointer rules, or how you keep objects alive is affected. So the correct one-line answer is: an implementation change to how the marker walks memory, invisible at the level of the language and the tuning surface. ## How to evaluate it for a real service Treat the headline percentage as a hypothesis about *someone else's* heap: 1. Build the same commit twice — default, and with `GOEXPERIMENT=nogreenteagc`. 2. Replay representative load against both, long enough to reach a steady live heap. Synthetic microbenchmarks with tiny live sets will show you nothing, because the effect is about memory locality over a real object graph. 3. Compare the share of CPU going to garbage collection and the tail latency of your requests, not just throughput. 4. Decide with the numbers. If the difference is noise for your workload, the interesting conclusion is that your GC cost is not marking-bound and you should look at allocation rate and live-set size instead. ## The judgment worth showing Green Tea is a free win when it is a win and a non-event otherwise, and it is not a substitute for the two things that actually dominate Go GC cost: **how much you allocate** and **how large and pointer-dense your live set is**. A team that reacts to a GC-heavy profile by upgrading toolchains and hoping is doing the less effective thing; reducing allocations in the hot path, reusing buffers, and flattening pointer-heavy structures into values move the same number further and keep moving it on the next release.

  • How would you decide whether Green Tea is actually helping your service?
    Build the same commit twice, once with `GOEXPERIMENT=nogreenteagc`, and run both under representative load with a realistic live heap. Compare the CPU share spent collecting and request tail latency, not throughput alone. Gains concentrate in large pointer-dense heaps; a service that rarely collects will show noise, which itself tells you your GC cost is not marking-bound.
  • Why can a garbage collector's marker be limited by memory latency rather than CPU?
    Tracing is pointer chasing: each hop lands at an address the caches have no reason to hold, so the core stalls waiting for memory instead of executing. Scanning objects that share a span together turns much of that random access into sequential access the prefetchers can anticipate, which is the whole idea behind span-oriented marking.
  • Does Green Tea change anything a Go developer has to do differently?
    No. The programming model, the tuning knobs, and the collector's guarantees are identical — same concurrent tri-colour marking, same lazy sweep, still non-generational and non-moving. It is an implementation change inside the marker, so the only action it invites is measuring whether your workload benefits.

saying these in an interview costs you the question

  • Says Green Tea made Go's collector generational
  • Claims it compacts or relocates heap objects
  • Thinks GOEXPERIMENT can be flipped at runtime without rebuilding
  • Quotes the 10-40% figure as a guarantee for any workload
  • Treats a toolchain upgrade as a substitute for reducing allocations