Headless Service: A Phone Directory Instead of a Switchboard
A Headless Service returns pod IPs directly instead of routing through a proxy. That one change fixes the CDP GUID race by pinning both calls to the same pod.
Regular vs Headless
Regular Service = a switchboard. You call one number. The operator routes you to a random desk. Two calls can land on different desks.
Headless Service = a phone directory. You get all extension numbers. You pick one and dial it directly.
Why This Fixes the GUID Race
The API pod looks up the Chrome Service in DNS. It gets all Chrome pod IPs (not one proxy IP). It picks one and uses that IP for both the GET and the WebSocket. Both calls land on the same Chrome. The GUID matches.
var chromePodRoundRobin atomic.Uint64
addrs, _ := net.LookupHost(hostname)
idx := chromePodRoundRobin.Add(1) % uint64(len(addrs))
podAddr := net.JoinHostPort(addrs[idx], port)
// use podAddr for BOTH GET and WebSocket
DNS returns only Ready pod IPs. Pending or failing pods do not appear. A new DNS lookup between retries can pick a different pod. The “escape a stuck pod” ability is preserved.
Inside One Request
Each request runs in its own goroutine. All five steps use the same local podAddr.
The round-robin counter (atomic.Uint64) is the only shared state. Everything else stays local: podAddr, GUID, the chromedp session. No mutex. No data races. See why fresh TCP matters for scaling.
clusterIP is immutable. You cannot change an existing Service from regular to headless. Kubernetes rejects the update. You must delete it and recreate it. ArgoCD needs
Replace=trueandForce=truesync options.
References:
Related: see why fresh TCP matters for scaling, the GUID race this fixes, the DNS capacity model, or go back to the series overview.