skip to content

In Go's 25% GC CPU target, how do dedicated, fractional and idle mark workers differ?

level: middleimportance: nice to knowfreq 24%

answer

  1. the budget is a fraction of GOMAXPROCS
  2. the whole part and the remainder
  3. one holds its P, one yields early
  4. a third runs only when nothing else will
  5. assists sit outside this budget

basics

~20 s

Go targets about 25% of GOMAXPROCS for background marking. Whole units become dedicated workers that hold a P for the entire mark phase; the leftover fraction becomes a fractional worker that yields when its goal is met; idle workers use otherwise-idle Ps.

solid answer

~50 s

Background marking is budgeted at roughly 25% of GOMAXPROCS, and that product is usually not a whole number of workers. The whole part is filled by dedicated mark workers: each takes a P for the entire mark phase and drains marking work until the phase ends, so on a four-P process one P is effectively handed to the collector. The leftover fraction is covered by a fractional mark worker, which is scheduled on a P, marks until it has met its utilisation goal for the period, then yields back to the program — it exists because rounding is a huge error when the budget is small. Idle mark workers are extra and opportunistic: they run only on a P with nothing runnable and give it up the instant a goroutine needs it, so they cost the program nothing. Mark assists are a fourth source of marking CPU, charged to allocating goroutines on top of the 25%.

go deeper

for a junior

Know that Go's collector reserves a share of the machine — roughly a quarter of the Ps — to mark in the background while your program keeps running, instead of stopping everything to collect.

for a middle

Be able to name the three background worker kinds and say what each does with its P: one holds it for the whole phase, one yields after meeting a utilisation goal, and one borrows only a P nobody else wants.

for a senior

Reason about the boxes you actually run on: how many Ps the process really has, how much of them marking takes during a cycle, and why measured garbage-collection CPU can sit above the background target when allocation is heavy.

for a principal

Treat the collector's share as capacity planning rather than trivia. A quarter of every container's CPU is provisionally spoken for during a cycle, and the CPU limit you set decides the marking capacity every service the team ships will have.

### The budget Go's collector spends its background marking effort against a target of roughly **25% of GOMAXPROCS** — that is, a quarter of the logical processors (Ps) available to the process. The number is a design choice, not a tuning knob you set directly: it is high enough for the collector to finish cycles on realistic workloads and low enough that the program always keeps three-quarters of its own machine. Everything below is about how that fractional budget is turned into actual goroutines doing actual marking. Multiply GOMAXPROCS by 0.25 and, in general, you get a number that is not a whole number of workers. Go covers the whole part one way and the remainder another. ### Dedicated mark workers The whole part becomes **dedicated mark workers**. A dedicated worker is scheduled on a P and keeps it for the entire mark phase, draining marking work until the phase is over. From the program's point of view that P is simply gone for the duration of the cycle. With GOMAXPROCS=4 the arithmetic is exact: 25% of four is one, so a single dedicated worker takes one P and the program's goroutines share the remaining three while marking runs. With GOMAXPROCS=8 there are two dedicated workers. This is the simplest and most efficient shape, because a worker that holds its P has no scheduling overhead and no hand-off cost. ### Fractional mark workers When the target is not a whole number of Ps, the leftover is covered by a **fractional mark worker**. Rather than holding a P for the whole phase, it is scheduled on a P, marks until it has met its utilisation goal for the current period, and then yields back to the scheduler so the program can use that P again. The reason this kind exists is that rounding is a big error when the budget is small. On a two-P process the target is half a worker; rounding up would hand the collector half the machine and rounding down would leave background marking with nothing at all. A worker that runs part of the time and yields hits the target without either extreme. ### Idle mark workers **Idle mark workers** are a third, opportunistic kind, and they sit *outside* the 25% accounting. An idle worker runs only on a P that has no runnable goroutine to execute, and it gives that P up the moment a goroutine becomes runnable. It therefore takes nothing away from the program: it converts CPU that would have gone unused into completed marking, which shortens the cycle and reduces the assists everyone else has to pay. The practical surprise is on the monitoring side. A process that is mostly idle can show meaningful garbage-collection CPU purely from idle marking. That is not a problem to be fixed; it is spare capacity being spent usefully. ### Mark assists: the fourth source, and the one that breaks the budget There is a fourth source of marking CPU, and it is why the phrase "Go's GC uses 25% of CPU" is not the whole truth. When background workers cannot keep marking ahead of the program's allocation rate, the runtime charges the allocating goroutine an amount of scanning proportional to the bytes it allocates and makes it do that work inline before the allocation returns. That is a mark assist, and it is executed by your goroutines on your Ps, on top of the background budget. So the 25% is a target for **background** marking, not a ceiling on total garbage-collection CPU. A service with a high allocation rate can measure garbage-collection CPU well above a quarter of the process, and the excess will be assist time attributed to application call stacks rather than to the collector. ### How to hold the whole picture - Dedicated: takes a P for the whole mark phase. Fixed, predictable, the bulk of background marking on large processes. - Fractional: takes a P briefly, meets a utilisation goal, yields. Exists to represent the non-integer remainder of the budget. - Idle: takes only a P nobody wants, yields instantly. Free marking, outside the budget. - Assists: not background workers at all. Charged to the allocating goroutine, unbounded by the 25% target, and the mechanism through which a fast allocator pays for the marker's backlog. ### Why it matters operationally The worker split follows GOMAXPROCS, so the number of Ps your process actually has determines how much marking capacity it has. In a container, that number is the one the runtime derived from the container's CPU limit, which may be far smaller than the host's core count. A service squeezed into a small CPU limit gets proportionally less background marking, which makes it more likely to fall back on assists — and assists are the part that lands in request latency.

  • If a Go process runs with GOMAXPROCS=4, how much of it does background marking take?
    About one P's worth. Twenty-five percent of four is exactly one, so the runtime uses a single dedicated mark worker and needs no fractional worker. That P is occupied by marking for the whole mark phase, leaving three for application goroutines, plus whatever extra marking idle workers manage on Ps that have nothing to run.
  • Do idle mark workers count against the 25% target?
    No. They are opportunistic: they run only on a P with no runnable goroutine and yield it immediately when one appears, so they take nothing from the program. They can make a mostly idle process show noticeable garbage-collection CPU, which surprises people reading CPU graphs, but that is spare capacity being converted into completed marking.
  • Why is a fractional worker needed at all rather than rounding to whole dedicated workers?
    Because rounding is a large error when the budget is small. On a two-P process the target is half a worker: rounding up would hand the collector half the machine, and rounding down would leave background marking with nothing. A worker that runs for part of the time on a P and then yields hits the target without either extreme.
  • Why can measured garbage-collection CPU sit well above 25% of the process?
    Because the 25% is a target for background marking only. When those workers cannot keep ahead of the allocation rate, allocating goroutines are charged mark assists and perform marking inline on their own Ps. That work is real garbage-collection CPU, it is unbounded by the background target, and it is attributed to application call stacks.

saying these in an interview costs you the question

  • Says the 25% target is a hard cap on all garbage-collection CPU
  • Describes dedicated mark workers as OS threads pinned to cores
  • Believes idle mark workers steal time from running goroutines
  • Assumes the worker count comes from the host's core count in a container
  • Confuses the collector's CPU budget with a pause-time goal