DEV Community

Akshath Agarwal
Akshath Agarwal

Posted on

Kubernetes Hands-on hardcore : The Workqueue isn't your bottleneck

Every Kubernetes controller tutorial says the same thing about client-go’s workqueue:
“It dedupes keys, it handles exponential backoff, and it’s almost never your performance bottleneck.”
I’ve said that exact line in design reviews more times than I can count. But a while back it hit me: I’d never actually watched it happen. I knew the theory from the docs and a bit of source diving, but I had zero real operational feel for how it behaved under load.
That bothered me enough to build a small tool so I could see it live.
The three things I wanted to observe
I wanted to watch three specific behaviours I could explain on a whiteboard but had never seen under pressure:

Queued deduplication
If a key is already sitting in the queue and Add() gets called twice more, does the second call just disappear, or is something subtler going on?

Processing set collisions
If a worker is currently processing a key and three new Add() calls arrive for the same key, does the queue drop them, stack them, or queue a single retry behind it?

Backoff math
Does per-key exponential backoff actually double on every failure, or is that just the idealised description in the go docs?

So I built Workerqueue lab : a Go tool that wraps workqueue.RateLimitingInterface and logs every internal state transition.
It has two modes:

Direct mode: Floods the queue with ~50k Add() calls/sec across a tiny set of 20 hot keys. This forces tons of collisions so you can watch deduplication and redelivery in real time.
Pipeline mode: The real end-to-end path. An HTTP endpoint does actual writes against a lightweight local kube apiserver (via envtest). Traffic flows through etcd → Cacher → SharedInformer → Workqueue. Real Kubernetes machinery, no simulation.

What surprised me

  1. The “dirty / processing set” behaviour in action Seeing [ENQUEUE] immediately followed by [DEDUP] tens of thousands of times a second was satisfying, but the redelivery behaviour was the real “aha” moment. If a worker is mid-reconcile on a key and three new Add() events arrive for that same key:

You don’t get three follow-up reconciles.
You get exactly one follow-up reconcile, and it fires the moment the worker calls Done().

That’s the dirty and processing sets doing their job. The queue doesn’t ignore the incoming events, and it doesn’t let redundant reconciles pile up. It collapses them into a simple guarantee: at least one more reconcile will run after the current one finishes, but never a burst of duplicates.
Watching sequence numbers climb per key while the worker go routines shuffled underneath made the abstraction finally click.

  1. The latency numbers that changed how I debug Pipeline mode gave me concrete numbers for each hop:

The latency numbers

The queue itself is roughly 1,000× cheaper than the API server write path around it.
If your controller’s queue depth is climbing in production, it is almost never a queue bottleneck. It’s either your reconcile logic blocking or API server latency under load.
Why build this instead of just reading the source?
I could already recite the concepts. Reading source gives you facts. Watching the state transitions under load gives you intuition.
When a production incident hits at 2 AM and queue depths are spiking, you don’t have time to re-read client-go. You need to know in ten seconds whether you’re looking at a queue problem or a reconcile problem.

A key's life as a state machine:

Life as a state machine

Repo: github.com/Akshath11/workerqueue-lab
Companion (LIST/WATCH → Cacher fan-out): github.com/Akshath11/informer-lab

If you work on Kubernetes platforms or prep for system design interviews that dig into controller internals, it’s worth an hour. Happy to answer questions if anyone tries it and hits something confusing.

Top comments (0)