What Happened to Go GC After 1.25 The “Green Tea” Story

I heard the word “Green Tea GC” maybe three or four times before I actually stopped and asked wait, what is this thing?

Honestly, first time I saw it in a forum I thought somebody was joking. Green Tea? In a technical discussion about garbage collection? I scrolled past it.

But then it kept coming. Again and again in the Go community. And slowly I realized okay, this is real. People are genuinely excited about this.

So I sat down one evening, made my tea (actual tea, not Green Tea GC), and decided to go deep.


First Thing - Why “Green Tea”?

This was my first question also.

Turns out it’s just a nickname. But the nickname actually explains the whole idea quite well.

Think about espresso. Strong. Hits hard. Done fast. No mercy.

Now think about green tea. Light. Smooth. Gentle on your body. You can drink it the whole day without feeling destroyed.

That’s the direction Go’s garbage collector is trying to move toward. Less aggressive. More balanced. Easier on the system overall.

Simple idea. But behind it, there’s a lot of engineering.


Let Me Start From The Beginning

To understand where Go is going, I had to go back to basics. Like, really back.

I wrote this simple line:

p := &Person{Name: "Shehan"}

That Person object lives somewhere in memory. On the heap, specifically.

Now here’s the question that actually troubled me when I thought about it properly:

Who frees this memory? And when is it actually safe to free it?

Sounds simple. But this is one of the hardest problems in computer science.


Two Ways To Solve It

There are really only two answers to this problem.

First way - manual memory management. Like C, like C++. You allocate the memory, you free it yourself. Full control. Very fast. No overhead from the runtime.

But also - very dangerous. One mistake, and you have a memory leak. Or a crash. Or corrupted data that you won’t even notice until your system is in production and something breaks at 2 AM. I don’t want that life.

Second way - garbage collection. This is what Go does. The runtime itself watches your objects and decides: “nothing can reach this anymore, so it’s safe to remove.”

You don’t have to think about it. It just happens.

Sounds like magic, no? But the constraints behind it are actually brutal.


The Rules Are Not Easy

Go’s garbage collector cannot just do whatever it wants. It has to follow some very strict rules.

  • It cannot pause your program for too long
  • It cannot cause sudden latency spikes
  • It has to handle millions of objects at the same time
  • And it has to do all of this while your code is still running

That last point is the hard one. Your code is not stopped. It’s running. And the GC is running alongside it. Both at the same time.

This is what makes it complicated.


How Go’s GC Actually Works - Tri-Color Marking

Go uses something called concurrent tri-color mark and sweep garbage collection.

The idea behind it is clever. Instead of asking “is this object being used?”, it asks a different question:

“Can I reach this object from somewhere important?”

Those “important places” are called roots. Stack variables, global variables, CPU registers - these are your roots.

From those roots, the GC walks through the entire object graph. Every object it can reach gets marked. Everything it cannot reach is considered garbage. At the end, the unmarked objects get swept away.

The “tri-color” part refers to how objects are labeled during this process - white (not yet visited), grey (being processed), and black (done). It keeps everything organized even when things are happening concurrently.


The Old Problem - Stop The World (STW)

Early garbage collectors had one big problem.

They would literally stop your entire program, scan all the memory, and then continue.

This is called Stop-The-World (STW). And for modern backend systems - microservices, APIs handling thousands of requests - this is completely unacceptable. Even a 100ms pause can ruin your latency numbers.

So Go evolved. Instead of stopping everything, the GC now runs alongside your program. In the background. Concurrently.

This was a huge improvement. But it introduced a new problem.


The New Problem - Changing Graph

Your program is not static. While the GC is scanning memory, your code is still running and changing things.

You might update a pointer. Create a new object. Modify a relationship between two objects.

This means the object graph is changing while the GC is trying to understand it.

If the GC misses a change, it might free an object that is still being used. That’s a bug. A very bad bug.


The Solution - Write Barriers

To fix this, Go uses something called a write barrier.

Every time your program updates a pointer, the write barrier fires - it tells the GC: “hey, something changed here. Don’t miss this.”

This keeps the whole system consistent even when both your code and the GC are running at the same time.

It adds a small overhead, yes. But it’s the price you pay for correctness under concurrency.


So Go’s GC Is Good. What’s The Problem?

At this point, Go’s garbage collector is already quite advanced. Concurrent, write barriers, all of that.

But there’s still one hidden inefficiency.

It treats every object the same.

Young objects, old objects, temporary objects, long-lived objects - same treatment for all.

And in real systems, this is wasteful.


The Key Insight - Most Objects Die Young

Think about something like this:

func handler() {
    temp := make([]byte, 1024)
    // ... use it for something
    // function ends, temp is gone
}

That temp object is created and destroyed almost immediately. It has a very short life.

But the current GC still tracks it, marks it, sweeps it - same as an object that has been alive for minutes.

That’s wasted effort. And at scale, that wasted effort adds up.


The Big Idea - Generational GC

Here comes the concept that changes everything.

What if instead of treating all objects the same, we separate them by their age?

This is called generational garbage collection. The idea is:

  • New objects go into a young generation
  • Most of them die quickly - cheaply collected
  • If an object survives long enough, it moves to the old generation
  • Old objects are stable - they don’t need to be scanned so often

Now the GC doesn’t have to scan the entire heap every time. It mostly focuses on the young generation, where most of the action is happening.

Less CPU usage. Less latency. Faster GC cycles.

This is exactly what “Green Tea” is pointing toward.


Then Why Didn’t Go Do This Before?

Good question. I asked this myself.

Because Go makes it hard.

Go programs are full of pointers. Maps, slices, interfaces - everything is connected to everything. Tracking those relationships is already complex.

Now imagine splitting memory into two generations and tracking which old objects are pointing to which young objects. That’s called a remembered set, and maintaining it is non-trivial.

On top of that, Go is highly concurrent. Hundreds of goroutines, all creating and modifying objects at the same time. The GC has to keep up with all of that.

And generational GC needs more write barriers, not fewer. Because now you also need to track when an old object points to a young one. That adds overhead.

So the challenge is not just making GC smarter. The challenge is making it smarter without making everything else slower.

That’s the engineering problem. That’s what makes this interesting.


What This Changed For Me

I’ll be honest. Before I went deep into this, GC was just a “the runtime handles it” topic for me.

I didn’t think much about it. Write the code, ship it, if something is slow - add more servers, maybe.

But now I see it differently.

GC is not just a background process. It directly affects your latency, your throughput, your system stability under load.

When your GC is behaving badly - long pauses, frequent cycles, high CPU - your users feel it. Your metrics feel it. And you feel it when you’re trying to debug it at night.


Where I Am Now

Right now I’m following a specific path to really understand this properly:

  1. Stack vs Heap and Escape Analysis - where does memory actually go, and why?
  2. GC Internals - tri-color marking, write barriers, the actual mechanics
  3. Generational ideas - where Go is heading and what trade-offs they’re making

This is not easy reading. But it’s the kind of understanding that separates engineers who write code from engineers who understand why the system behaves the way it does under pressure.


One Last Thing

If you’re also on this path - learning Go deeply, not just the syntax but the internals - I think this is one of the best places to spend your time right now.

Green Tea GC is still evolving. The Go team hasn’t shipped a full generational GC yet. But the direction is clear.

And if you understand the foundation now - how memory works, how the GC thinks, what trade-offs exist - you’ll be in a much better position to reason about your systems when this lands.

That’s the goal, no?

Not just to write Go. But to actually understand it.


Thanks for reading. If something here was wrong or you want to discuss further, find me - I’m still learning this too.