Section 01
Three sets, and the invariant that ties them
The workqueue is not a list with a deduplication check bolted on. It is three structures maintained together, and the comment on the first one states the invariant the other two exist to preserve.
// queue defines the order in which we will work on items. Every // element of queue should be in the dirty set and not in the // processing set. queue Queue[t] // dirty defines all of the items that need to be processed. dirty sets.Set[t] // Things that are currently being processed are in the processing set. // These things may be simultaneously in the dirty set. When we finish // processing something and remove it from this set, we'll check if // it's in the dirty set, and if so, add it to the queue. processing sets.Set[t]
dirty answers “does this need work?”. processing answers “is someone on it?”. queue is only the ordering, and it is allowed to hold an item only when the first answer is yes and the second is no. Every behaviour in this chapter falls out of maintaining that.
Add arriving while a worker holds the key re-enters dirty but is forbidden from touching queue. Done is the only thing that can move it back, and only if dirty still has it.Section 02
One hundred thousand adds, one entry
Deduplication is a single set lookup at the top of Add. It costs one hash probe and it is unconditional.
func (q *Typed[T]) Add(item T) {
...
if q.dirty.Has(item) {
// the same item is added again before it is processed, call the Touch
// function if the queue cares about it (for e.g, reset its priority)
if !q.processing.Has(item) {
q.queue.Touch(item)
}
return
}
q.metrics.add(item)
q.dirty.Insert(item)
if q.processing.Has(item) {
return
}
q.queue.Push(item)
q.cond.Signal()
}
A. Add("default/target") x 100000
queue Len() = 1
There are two separate early returns in that function and they do different jobs. The first (dirty.Has) is deduplication. The second (processing.Has) is the single-writer lock. An item can be dirty and processing at once — that is the state the whole design hangs on, and it is why the two sets cannot be collapsed into one.
Section 03
Where adds go while a worker holds the key
The interesting case is not a quiet queue. It is 200 events arriving for an object that is being reconciled right now — which is the normal condition under load, as chapter 01 measured.
B. one worker holds the key, then 200 more Add()s arrive Len() while key is held, after 200 adds : 0 <- adds went to `dirty`, not the queue Len() observed by the worker before Done: 0 Len() immediately after Done() : 1 <- Done() re-queued it, once further Get()s needed to drain : 1 <- 200 adds produced exactly this many reconciles
func (q *Typed[T]) Done(item T) {
...
q.processing.Delete(item)
if q.dirty.Has(item) {
q.queue.Push(item)
q.cond.Signal()
} else if q.processing.Len() == 0 {
q.cond.Signal()
}
}
dirty, which Len() does not count.Len() counts the queue, not dirty. During the busiest moment for a given key, its depth contribution is zero. Queue depth is a reasonable signal for “work is backing up across many keys” and a poor one for “how much has changed” — those differ by exactly the absorbed adds.
Section 04
Serial per key, parallel across keys
The controller-writing guide promises that “no two goroutines will work on the same item at the same time.” That is a concurrency claim, so it deserves a concurrency test: sixteen workers, sixty-four keys, eight producers, and a detector that flags any instant two workers hold the same key.
queue implementation : client-go workqueue workers : 16 distinct keys : 64 Add() calls from 8 producers : 320000 reconciles actually executed : 148497 collapse ratio : 2.2x max distinct keys in flight : 14 (workers=16) SAME-KEY CONCURRENCY VIOLATIONS : 0 <- must be 0 elapsed : 1.793s
Both halves of the guarantee are visible in one run. Zero same-key violations, so the per-key serialisation holds. And 14 distinct keys in flight at peak against 16 workers, so the queue is not serialising globally — it parallelises freely across keys while locking per key.
This is what makes raising MaxConcurrentReconciles safe by default. You get more throughput across objects without ever needing a per-object mutex of your own, because processing already is one.
Section 05
The control that fails on purpose
A detector that reports zero is worthless until you have seen it report something. The same harness, with the workqueue swapped for a plain buffered channel — no dirty, no processing, no Done — is the control.
queue implementation : plain channel (CONTROL, expected to fail) workers : 16 distinct keys : 64 Add() calls from 8 producers : 320000 reconciles actually executed : 320000 collapse ratio : 1.0x max distinct keys in flight : 16 (workers=16) SAME-KEY CONCURRENCY VIOLATIONS : 21940 <- must be 0 elapsed : 576ms
The control is the reason the zero in §04 means anything. The same detector, the same workers, the same key distribution, and it fires 21,940 times when the two sets are removed. A passing test that has never been seen to fail is not evidence.
Section 06
Further reading
All links returned HTTP 200 on 2026-09-11 unless noted.
Section 07
Closing note — what varies, and what was not verified
Durable versus run-specific
- Durable:
Len() == 1after 100,000 adds;Len() == 0while the key is held regardless of how many adds arrive;Len() == 1immediately afterDone; exactly one further reconcile; 0 same-key violations with the workqueue and a non-zero count with the channel control. - Run-specific: 148,497 reconciles, the 2.2× collapse ratio, 14 keys in flight, 21,940 violations, and both elapsed times. All depend on scheduler timing, core count, and the 20 µs artificial hold. The signs are durable: workqueue collapse > 1× with 0 violations; channel collapse = 1.0× with many.
Traps this chapter had to avoid
- A detector that cannot fail. Reporting “0 violations” from a harness never observed failing proves nothing — it is indistinguishable from a broken detector. The
-naivecontrol exists solely to make the zero meaningful, and it is a demo designed to fail. - Holding the key too briefly to overlap. With no artificial hold, workers finish before another can collide, and even the broken control reports zero. The 20 µs sleep exists to make overlap possible; without it the test is vacuous in both arms.
- Reading
Len()as “outstanding work”. It is the queue slice only. During part B it reads 0 while 200 changes are pending.
Not verified
- The priority queue. §02–§05 measure the plain client-go
Typedworkqueue. controller-runtime defaults to its own priority queue (UsePriorityQueue, default true), which wraps the same dirty/processing discipline but adds ordering by priority. Chapter 05 covers it; nothing here measured it. - Rate limiting and delayed adds.
AddRateLimited,AddAfterand the backoff curve are chapter 05's subject and were not exercised here. - Metrics behaviour.
q.metrics.add/doneappear in the quoted source; no metrics provider was registered, so their behaviour was not observed. - Shutdown semantics.
ShutDownversusShutDownWithDraindiffer in whether in-flight items are awaited; only the former was used, and not tested for drain behaviour.
Section 08
Spoken drills
Why does the workqueue need two sets rather than one? What state is impossible to express with one?
A strong answer hits
- An item must be able to be dirty and processing simultaneously — work owed on something already in flight
- One set cannot distinguish “needs work” from “someone is on it”
Addhas two early returns doing two different jobs: dedup, then the single-writer lockDoneis the only transition that can re-queue, and only when dirty still holds it- The honest half:
queueis a third structure but carries no semantics — it is purely ordering, and the invariant says it may only hold items that are dirty and not processing
check against §01 and §02
Your queue-depth dashboard reads zero during an incident where a controller is visibly behind. Is the dashboard broken?
A strong answer hits
- No —
Len()counts the queue slice only, notdirty - While a key is being processed, every add for it lands in
dirtyand is invisible toLen() - Measured: 200 adds arrived and
Len()stayed at 0 throughout - Depth is a signal about many keys backing up, not about how much changed
- The honest half: for a controller with many keys, depth is still a useful backlog signal — the blind spot is per-key, and it is worst exactly when one hot object is being hammered
check against §03
A colleague wants a per-object mutex in the reconciler “so two workers can’t stomp on each other.” Respond.
A strong answer hits
- The
processingset already is that mutex, keyed by the queue item - Measured: 16 workers, 320,000 adds, 0 same-key violations under
-race - Parallelism across keys is preserved — 14 distinct keys in flight at peak
- The control proves the guarantee is doing the work: a plain channel gave 21,940 violations
- The honest half: it only holds per queue item. Two different controllers watching the same object, or a reconciler that writes objects other than its request key, are outside the guarantee entirely
check against §04 and §05
You see “0 same-key violations” in a test report. What is the first question you ask?
A strong answer hits
- Has this detector ever been observed failing? A zero from an untested detector is not evidence
- Is the work held long enough for an overlap to be possible at all?
- Here: the
-naivecontrol fires 21,940 times, and the 20 µs hold makes overlap reachable - The honest half: the control also changes the collapse ratio to 1.0×, which is a second, independent signal that the two arms really are different — a control that matched on every axis but the violation count would be more suspicious, not less
check against §05