An HTTP load balancer built from scratch in Go with a real-time web dashboard. It supports three load balancing algorithms (round robin, weighted round robin, least connections), active health checking with automatic failover and recovery, and dynamic server management — spin up new backend server processes from the UI, and they auto-register into the pool. The dashboard visualizes the full request flow with animated packets and lets you drag servers, simulate traffic, switch algorithms, and kill/revive backends to observe failover behavior.
Load Balancing Algorithms
Round robin with atomic counter — lock-free rotation across healthy backends
Weighted round robin — backends added to a weighted list proportionally, with health-aware skipping on unhealthy servers
Least connections — tracks active connection count per backend, routes to the server with fewest in-flight requests
Health Checking & Failover
Background goroutine polls /health on every backend at a configurable interval with per-check timeouts
Consecutive failure threshold — 3 failed checks mark a server unhealthy and remove it from rotation
Automatic recovery — a single successful health check resets the failure counter and re-adds the server
Dynamic Server Management
Spin up backend servers on-demand — process manager spawns child processes with auto-assigned ports
Readiness gate — waits up to 5 seconds for the new server to pass a health check before adding it to the pool
RESTful management API — add/remove servers, change algorithms, update weights, and spin up servers at runtime
Real-time topology visualization with animated SVG packets showing request flow from client → load balancer → backend
Traffic simulation with configurable request count, delay, and processing time to observe algorithm behavior
On the roadmap
IP hash algorithm — route requests from the same client IP to the same backend for session affinity
Circuit breaker pattern — open the circuit after repeated failures to prevent cascading load on a degraded backend
WebSocket proxying — upgrade support so the load balancer can handle persistent connections, not just HTTP request-response
The thought process
The interesting constraint was concurrency. Every component — the backend pool, the algorithm state, the health checker, the process manager — runs concurrently and mutates shared state. The pool needed a sync.RWMutex so health checks and request routing could read simultaneously without blocking each other. The least connections algorithm required its own lock for the connection counter, separate from the pool lock, to avoid contention. Getting the locking granularity right was the real design work.
Tech stack & why
Go
Standard library net/http for the proxy and API server, httutil.ReverseProxy for request forwarding, goroutines for concurrent health checks and process management.
Go sync primitives
sync.RWMutex for thread-safe backend pool access, sync/atomic for lock-free round-robin counters, separate mutexes per component to minimize contention.
os/exec
Process manager spawns backend servers as child processes with automatic port assignment, readiness polling, and graceful shutdown on SIGINT/SIGTERM.
Vanilla JS + HTML/CSS
Dashboard with SVG-based topology visualization, animated request packets, draggable server nodes, and real-time state polling — no frameworks, no build step.
Major updates
Jun 2026
Docker & polish
Multi-stage Dockerfile, UI refinements, DNS fix for dynamic servers (127.0.0.1 instead of localhost to avoid resolution timeouts).
May 2026
Dynamic servers & dashboard
Process manager for spawning backend servers on-demand. Web dashboard with SVG topology, animated packets, drag-and-drop, and traffic simulation.
May 2026
Core load balancer
Load balancer with round robin, weighted round robin, and least connections. Backend pool with health checking, automatic failover, and recovery. RESTful management API.
What I actually learned
Mutex granularity matters more than you think — a single global lock caused health checks to block request routing. Splitting into per-component RWMutexes fixed the contention.
Dynamic server spinning needs a readiness gate. Spawning a process and immediately adding it to the pool caused requests to hit a server that was not listening yet.
Using localhost instead of 127.0.0.1 in Docker caused DNS resolution timeouts. A small config detail that took longer to debug than the entire health checker.
What I'd do differently
Connection pooling
Each request creates a new reverse proxy instance. Reusing proxies with persistent connections would reduce overhead on high-throughput workloads.
Weighted round robin rebuild
The weighted list is built at initialization time. Updating weights at runtime requires reconstructing the load balancer instance — a hot-rebuild would be smoother.
Key takeaways
01
Concurrency primitives are the real API design. The interface between components in a concurrent system is not function signatures — it is which locks protect which data.
02
Visualizing the system changed how I built it. Watching animated packets hit a dead server made failover bugs obvious in a way that log files never did.