Go's garbage collector marks concurrently — which phases of a GC cycle still stop the world, and what happens inside them?
answer
- two brackets, not one long freeze
- one at each end of marking
- something has to be switched on and off
- no heap tracing happens inside a pause
- sweeping is lazy, not a pause
basics
~20 sA Go GC cycle stops the world twice, briefly: once to finish the previous cycle's sweep and switch the write barrier on, and once at mark termination to end marking, switch the barrier off and flush per-P caches.
solid answer
~40 sGo's collector is concurrent, but each cycle is bracketed by two short stop-the-world pauses. The first, sweep termination, stops every goroutine, finishes any spans left unswept from the previous cycle, and turns the write barrier on before the world restarts — the barrier has to be active on every P before a single object is marked. Marking then runs alongside the application. When the marker runs out of work, mark termination stops the world again to finish that bookkeeping, turn the write barrier off and flush per-P caches. Sweeping afterwards is lazy and concurrent, done as allocation demands spans. Neither pause traces the heap, so pause length is driven by how quickly every goroutine can be brought to a stop and by per-P housekeeping, not by how big the heap is.
go deeper
Be ready to say that Go's collector runs alongside your goroutines and that a cycle takes two brief pauses rather than one long freeze. Knowing that marking and sweeping are concurrent is enough at this level.
An interviewer expects you to name what each pause is for — enabling the write barrier before marking begins, and closing marking out afterwards — and to explain why neither pause traces the heap.
Show you can reason about pause behaviour in production: the pause includes stopping every goroutine, so CPU starvation or a slow rendezvous lengthens what the application feels beyond what the collector itself did.
Own the framing of the trade. Go buys short, heap-size-independent pauses by moving collection work onto threads that run with the application, so short pauses do not mean cheap collection — the cost shows up as throughput during the cycle.
## What `concurrent collector` actually means here A garbage collector has to find every object the program can still reach and reclaim the rest. A simple collector does that with the program frozen — a *stop-the-world* (STW) pause covering the whole collection, which on a large heap means tens or hundreds of milliseconds. Go does not do that. Go's collector traces the heap while application goroutines keep running and allocating, and it stops the world only twice per cycle, for a very short time each. ## The four stages of one cycle **1. Sweep termination (stop-the-world).** All goroutines are brought to a halt. Any memory spans left over unswept from the *previous* cycle are finished off, because a new marking phase cannot begin over a half-swept heap. Before the world restarts, the runtime switches the write barrier on across every P (the runtime's logical processors) and enables the machinery that lets allocating goroutines help with marking. This switch is exactly why a pause is needed: it must be impossible for one goroutine to be marking while another is still doing unbarriered pointer writes, so the flag flip happens with nothing running. **2. Mark (concurrent).** The world restarts. The collector scans the roots — package-level variables and each goroutine's stack — and then traces outward through the heap, marking what it reaches. Application goroutines run throughout, mutating the very graph being traced; the write barrier is what keeps that safe. This is the long part of the cycle, and it is not a pause. **3. Mark termination (stop-the-world).** When there is no marking work left, the world stops again for a short pause to close the phase out: the write barrier is turned off, per-P caches are flushed, and the accounting for the next cycle is prepared. Again no heap tracing happens here — the tracing is already finished. **4. Sweep (concurrent and lazy).** The world runs. Memory whose objects were not marked is reclaimed span by span, mostly on demand as allocation asks for fresh spans, which is why some sweeping can still be outstanding when the next cycle's first pause arrives. ## Why the pauses stay short The two pauses do not contain any work proportional to the size of the live heap. They contain flag flips and per-P bookkeeping, so a program with a 100 MB heap and a program with a 100 GB heap take similar pauses. What *does* affect them is how long it takes to bring every goroutine to a stop — the world is not stopped until each one reaches a point where the runtime can safely suspend it — and how many Ps must be flushed. This is why a machine that is CPU-starved, or a process sharing cores with noisy neighbours, can observe a pause longer than the collector's own work: the tail is scheduling latency, not tracing. Go's collector is also non-moving: it never relocates a live object, so there is no evacuation or compaction phase that would have to stop the world to fix up pointers. That is a deliberate trade — Go accepts heap fragmentation in exchange for never needing a pause proportional to the amount of live data. ## The claims that are wrong - *Go stops the world for the whole collection.* No — only for the two short brackets. - *Pause time grows with the heap.* No — tracing is concurrent, so heap size drives cycle *length* and CPU cost, not pause length. - *Sweeping is a pause.* No — it is concurrent and mostly lazy. - *Go is pause-free.* Also no. Two short pauses per cycle are real, and if your latency budget is a few hundred microseconds at the tail, they are visible. ## What to take away Go moved the expensive part of collection out of the pause and onto threads that run alongside your program. The bill does not disappear; it is paid as CPU spent by the collector and as extra work the application's own goroutines do on every pointer write while marking is in progress. That is the shape of the trade: predictable sub-millisecond pauses, bought with throughput during the cycle.
- Why don't Go's stop-the-world pauses grow as the heap gets bigger?Because nothing inside them traces the heap. Both pauses do fixed-ish work: switching the write barrier on or off and flushing per-P bookkeeping. The tracing that scales with live data happens in the concurrent mark phase, and reclamation happens in the concurrent lazy sweep. A larger heap makes cycles longer and more CPU-hungry, not the pauses longer.
- What can make an observed pause longer than the collector's own work?The world is not stopped until every goroutine has been brought to a halt, so the pause includes that rendezvous. If the process is CPU-starved, sharing cores with other work, or a goroutine is slow to reach a suspendable point, the wall-clock pause the application feels is longer than the bookkeeping the collector actually does.
- Why does turning the write barrier on require stopping the world at all?Because marking must never overlap with unbarriered pointer writes. If some Ps had the barrier on and others did not while the marker was already running, a goroutine on a barrier-less P could hide a live object from the marker. Flipping the switch with nothing running removes that window.
Think of a stocktake done while the shop is open: you close the doors for a moment to hand every assistant a clipboard, count with customers still moving things around, then close briefly again to collect the clipboards.
saying these in an interview costs you the question
- Says Go stops the world for the entire collection
- Claims pause length grows with heap size
- Thinks sweeping happens inside a stop-the-world pause
- Describes Go's collector as completely pause-free
- Believes the collector compacts the heap during a pause