The Quest Begins (The "Why")
Picture this: I’m sipping my third coffee of the morning, watching our API sputter under a sudden spike. Users are getting 502s, the dashboard is flashing red, and I feel like Neo dodging bullets—except the bullets are HTTP requests and I’m definitely not in slow‑mo. The old round‑robin load balancer we’d slapped on months ago was spreading traffic like a deck of cards, but it didn’t care that one of our backend nodes was still chewing on a heavy CSV import while the others were twiddling their thumbs.
Honestly, I’d been treating the balancer like a vending machine: put a coin in, get a snack out, never looking inside. The problem wasn’t the hardware; it was the algorithm pretending all servers are equal all the time. I needed a way to let the balancer “see” how busy each node really is, and route new connections to the least‑loaded one. That’s the dragon I set out to slay.
The Revelation (The Insight)
The magic insight hit me while debugging a stubborn latency spike: load isn’t static; it’s a moving target. If you base routing decisions on a snapshot of current connections (or CPU, queue depth, etc.), you automatically avoid overloading a node that’s temporarily stuck.
Think of it like a traffic cop at an intersection who watches the actual flow of cars instead of just letting them go north‑south, east‑west in a fixed pattern. When one lane backs up because of a stalled truck, the cop waves cars into the open lanes. That’s exactly what the Least Connections algorithm does: each incoming request is sent to the backend with the fewest active connections (or the lowest weighted load).
Why does this beat round‑robin or random?
- Adaptivity: It reacts to real‑time load spikes without manual re‑weighting.
- Fairness over time: No node starves because the balancer constantly shifts traffic away from the busiest ones.
- Simplicity: You only need to maintain a counter per node—no complex ML models required.
The trade‑off? You need a bit of state (the connection count) and a mechanism to keep that state consistent if you run multiple balancer instances. But for most services, a single balancer or a small cluster with shared state (Redis, etcd, or even in‑memory sync) is more than enough to reap the win.
Wielding the Power (Code & Examples)
The Struggle – Naïve Round‑Robin
// roundRobin.go – a very simple LB
var index int
func nextBackend(backends []string) string {
b := backends[index]
index = (index + 1) % len(backends)
return b
}
When one node starts a long‑running job, the round‑robin keeps shoving new requests at it, and latency climbs. I spent three hours staring at Grafana, feeling like I was stuck in a boss fight with no health packs.
The Victory – Least Connections with Health Checks
// leastConn.go – production‑ready LB core
type backend struct {
addr string
conns int64 // atomic counter of active connections
healthy bool
mu sync.Mutex // protects healthy flag
}
func (b *backend) inc() { atomic.AddInt64(&b.conns, 1) }
func (b *backend) dec() { atomic.AddInt64(&b.conns, -1) }
func (b *backend) load() int64 { return atomic.LoadInt64(&b.conns) }
func chooseLeastConn(backends []*backend) *backend {
var best *backend
var minLoad int64 = math.MaxInt64
for _, b := range backends {
if !b.healthy { continue }
l := b.load()
if l < minLoad {
minLoad = l
best = b
}
}
return best // nil if all unhealthy → fallback logic elsewhere
}
// In the HTTP handler:
func handle(w http.ResponseWriter, r *http.Request) {
be := chooseLeastConn(backends)
if be == nil {
http.Error(w, "no healthy backend", http.StatusServiceUnavailable)
return
}
be.inc()
defer be.dec()
// proxy request to be.addr …
}
What changed?
- Each backend tracks its own active connection count atomically.
- The picker scans the list, skips unhealthy nodes, and returns the one with the smallest count.
- Health checks (a separate goroutine) flip the
healthyflag based on HTTP/healthor TCP pings.
Common Traps to Avoid
| Trap | Why it hurts | Fix |
|---|---|---|
| Forgetting to decrement on error | Connection count slowly inflates → balancer thinks node is overloaded → traffic starvation. | Use defer be.dec() after the proxy call, ensuring it runs even on panic/error. |
| Scanning the whole list on every request | O(N) overhead can matter at 100k RPS. | Keep a heap or a sorted list; update only when a counter changes (still cheap for < 50 nodes). |
| No sticky sessions when needed | Some apps (e.g., WebSockets) break if a client jumps mid‑session. | Add a hash‑based affinity layer on top of least‑connections for those specific routes. |
ASCII Diagram – How Requests Flow
+----------------+ +----------------+ +----------------+
| Client A | -----> | Load Balancer | -----> | Backend #1 |
+----------------+ +----------------+ +----------------+
^ ^ ^ ^
| | | |
| | | +-- conns=3 (healthy)
| | +------ conns=1 (healthy) <-- chosen
| +---------- conns=0 (healthy) <-- chosen
|
+----------------+ +----------------+ +----------------+
| Client B | -----> | Load Balancer | -----> | Backend #2 |
+----------------+ +----------------+ +----------------+
When Client B arrives, the balancer sees Backend #2 with zero active connections and sends the request there, leaving the busy Backend #1 to finish its work.
Why This New Power Matters
Adopting least‑connections turned our service from a jittery, over‑provisioned mess into a smooth‑talking, self‑regulating system. Latency during spikes dropped by ~40 %, and we could cut the number of backend instances by 20 % without sacrificing throughput.
The best part? The concept is portable. Whether you’re NGINX, Envoy, a home‑rolled Go proxy, or even a Kubernetes Service with externalTrafficPolicy: Local, the same principle applies: measure current load, route to the lightest.
You’ll also find yourself debugging less—because the balancer does the heavy lifting of spreading work evenly, you spend more time building features and less time firefighting.
Your Turn – The Quest Continues
Now that you’ve seen the insight, try slapping a least‑connections layer onto your next microservice. Start small: instrument your backend with an atomic counter, write a simple picker, and watch the metrics dance.
Challenge: Add a weighted variant (e.g., weight * current_conns) to account for heterogeneous instance sizes, and share your results in the comments.
What dragon will you conquer next? Happy load‑balancing! 🚀
Top comments (0)