A configurable Layer-7 HTTP reverse-proxy load balancer written in Go. It fronts a pool of backend servers, forwards incoming HTTP requests to a backend chosen by a pluggable selection strategy, and periodically health-checks the backends so traffic is only sent to reachable ones. The backend list is loaded from a config file and hot-reloaded when that file changes on disk, so servers can be added or removed without restarting the balancer.
- HTTP reverse proxy built on the standard library's
httputil.NewSingleHostReverseProxy. - Eight selection strategies, chosen by the
strategyconfig field:roundRobin— cycles through alive backends.weightedRoundRobin— distributes proportionally to per-server weight.leastConnection— picks the alive backend with the fewest active connections.weightedLeastConnection— picks by lowest connection/weight ratio; increments/decrements the connection counter around the request.leastResponseTime— picks the backend with the lowest exponentially-smoothed average response time.weightedResponseTime— picks by lowest avg-time/weight score.IpHash— hashes the client IP (FNV-32a) to consistently map a client to a backend.random— uniformly random alive backend (seeded RNG behind a mutex).
- Active health checking on a fixed interval: each backend is probed with a TCP dial (2s timeout) and marked alive/dead; strategies skip dead backends.
- Config hot-reload via filesystem watching (
fsnotify): on a write/create event to the config file, the config is re-parsed and the backend pool is rebuilt live. - JSON or YAML config, selected automatically by file extension.
- Thread-safe config and server state using
sync.RWMutex/sync.Mutexand atomics. - Bundled test backends (Bun + Hono TypeScript apps) for exercising the balancer locally.
Client ──► http.ServeMux "/" handler (cmd/main.go)
• extract client IP (host part of RemoteAddr) into request context
• serverManager.GetServer(ctx, pool.Servers) → chosen backend URL
• (strategy-specific connection / avg-time bookkeeping)
• httputil.NewSingleHostReverseProxy(backend).ServeHTTP(w, r)
──► backend server
On startup main loads the config, seeds the ServerPool, selects a ServerManager implementation from the strategy string via GetServerManger, starts the health-check goroutine, starts the config watcher goroutine, and serves HTTP on the configured port. Each strategy implements the ServerManager interface (GetServer(ctx, []*Server) (url.URL, error)); some also implement optional UpdateServerStats (connection decrement) and UpdateAvgTime (response-time update) interfaces that the handler invokes when supported.
healthCheck runs a time.Ticker at healthCheckInterval seconds. On each tick it calls ServerPool.HealthCheck, which for every backend performs a net.DialTimeout("tcp", host, 2s); success marks the server alive, failure marks it dead. Selection strategies filter on this IsAlive flag, so only reachable backends receive traffic.
Reload is driven by filesystem events, not a SIGHUP handler. config.WatchConfig creates an fsnotify watcher on the config file path; when a Write or Create event fires it calls Config.ReloadFromFile, then clears and repopulates the ServerPool from the new server list under lock. Config guards its fields with a sync.RWMutex, and SafeGet returns a deep copy of the current config for safe concurrent reads.
- HTTP requests are served concurrently by
net/http; each strategy is safe for concurrentGetServercalls (round-robin uses an atomic counter, weighted round-robin and random use mutexes). - Health checking and config watching each run in their own goroutine.
Serverstate (alive flag, connection count) is protected by a per-serversync.Mutex;ServerPoolmutations are guarded by its own mutex.
models.Server— backend URL, connection count, smoothed average time, alive flag, weight, and a mutex.models.ServerPool— the slice of*Server, withAddServer,ClearServers, andHealthCheck.
- Go (module targets
go 1.23.4), using only the standard library for HTTP proxying (net/http,net/http/httputil), hashing (hash/fnv), and concurrency primitives. github.com/fsnotify/fsnotify v1.9.0— filesystem watching for config hot-reload.gopkg.in/yaml.v3 v3.0.1— YAML config parsing (JSON handled byencoding/json).- Test backends: TypeScript apps using the Hono framework, run with Bun (
test/servers/server_1..4).
Prerequisites: Go 1.23+. (Optional: Bun to run the sample backends.)
# From the repo root — the app reads ./config.yaml by default
go run ./cmd
# Or build a binary
go build -o load-balancer ./cmd
./load-balancerThe config path is config.yaml (hard-coded in cmd/main.go); a config.json with the same schema is also included and would be parsed if that path were used (parsing is chosen by file extension).
Each test/servers/server_N is a Bun + Hono app listening on ports 7891–7894 (matching the default config). For example:
cd test/servers/server_1
bun install
bun run src/index.ts # serves on :7891Start all four, then run the balancer on :8080 and send requests to http://localhost:8080/.
config.yaml (or config.json):
port: "8080" # balancer listen port
strategy: "leastConnection" # one of the eight strategy names below
healthCheckInterval: 15 # seconds between health checks
servers:
- "http://localhost:7891"
- "http://localhost:7892"
- "http://localhost:7893"
- "http://localhost:7894"Valid strategy values: roundRobin, weightedRoundRobin, leastConnection, weightedLeastConnection, leastResponseTime, weightedResponseTime, IpHash, random.
curl http://localhost:8080/Every request to any path is proxied to a backend selected by the configured strategy. Edit config.yaml while the balancer is running to add or remove backends; the watcher reloads the server pool automatically.
Notes:
- The
serversconfig carries only URLs.Server.Weightis a per-server field that is not populated from this config schema, so the weighted strategies (which requireWeight > 0) depend on weights being set for backends to be selectable. - The active strategy, port, and health-check interval are read once at startup; hot-reload updates the backend list.
go-load-balancer/
├── cmd/main.go # entrypoint: load config, wire strategy, HTTP proxy, health check, watcher
├── config.yaml # default config (read at startup)
├── config.json # equivalent JSON config
├── internals/
│ ├── algo/ # selection strategies (one file each)
│ │ ├── RoundRobin.go
│ │ ├── WeightedRoundRobin.go
│ │ ├── LeastConnection.go
│ │ ├── WeightedLeastConnection.go
│ │ ├── LeastResponseTime.go
│ │ ├── WeightedResponseTime.go
│ │ ├── IpHash.go
│ │ └── Random.go
│ ├── config/
│ │ ├── config.go # Config struct, ReloadFromFile (JSON/YAML), SafeGet
│ │ └── watcher.go # fsnotify watcher → reload + rebuild pool
│ └── models/
│ ├── Server.go # backend state + TCP health probe
│ └── ServerPool.go # pool management + HealthCheck
└── test/servers/server_1..4/ # Bun + Hono sample backends (ports 7891–7894)