A Layer 7 reverse proxy and load balancer written in Go to explore concurrent systems programming and networking internals. The project supports host-based routing, Round Robin and Least Connections load balancing, automatic health checks, connection reuse, adaptive request admission, and graceful backend failover. Particular emphasis was placed on minimizing lock contention, benchmarking throughput, and validating behavior under backend failure scenarios.
-
Host-Based Routing β Routes incoming traffic to isolated backend clusters based on the request's
Hostheader (e.g.,api.localhost:8080vswww.localhost:8080), allowing multiple services or environments to share a single proxy without path rewriting. -
Buffered Channel Admission Control β Uses a buffered
chan struct{}as a lightweight semaphore to cap concurrent in-flight requests. Since Go's HTTP server already creates goroutines per request, this limits concurrency without introducing a worker pool. -
Timed Admission Queue β Rather than rejecting requests immediately when capacity is exhausted, requests wait up to 25ms for an available slot before returning HTTP 503 if the queue remains full.
-
Adaptive Request Deadlines β Dynamically adjusts request read deadlines based on
Content-Length, allowing larger uploads additional time while enforcing a 5-second limit for unknown payload sizes to reduce exposure to slowloris-style attacks. -
Context Propagation β Uses
http.NewRequestWithContextso backend requests are cancelled immediately when the client disconnects, preventing unnecessary backend work. -
Atomic Backend Snapshots β Routing state is published through
atomic.Value, allowing lock-free reads while health checks atomically replace failed backend snapshots without interrupting request handling. -
Connection Pool Management β Configures
http.Transportidle connection limits based on backend count, ensuring each backend maintains reusable persistent connections and reducing repeated TCP connection setup. -
Streaming Response Forwarding β Streams backend responses directly to clients while preserving status codes and headers with minimal buffering and allocation overhead.
Benchmarks were performed on an AMD Ryzen 7 9800X3D using a custom Go flood tool issuing 50,000 HTTP requests through a shared keep-alive client with 500 concurrent goroutines. All tests were executed on localhost to measure proxy throughput under controlled conditions.
| Metric | Result |
|---|---|
| Throughput | ~79,000 req/sec |
| Total requests | 50,000 |
| Concurrency | 500 goroutines |
| Successful | 49,855 |
| Failed/Timeout | 145 (queue capacity reached) |
| Distribution | 16,618 / 16,618 / 16,619 across 3 backends |
A one-request difference across 50,000 requests confirms the atomic Round Robin counter distributed traffic evenly. The 145 failed requests are expected because max_queue is intentionally limited to 100, causing requests beyond that admission limit to return HTTP 503.
With one backend terminated during the benchmark, the load balancer automatically removed it from the routing pool and redistributed traffic across the remaining healthy servers without manual intervention.
| Metric | Result |
|---|---|
| Throughput | ~70,800 req/sec |
| Distribution | 24,975 / 24,975 across 2 backends |
| Metric | Result |
|---|---|
| Throughput | ~73,100 req/sec |
| Distribution | ~16,500 / ~16,981 / ~16,408 across 3 backends |
Minor variance is expected because Least Connections routes each request to the backend with the fewest active requests at that moment. As requests complete at different times, the final request totals naturally diverge slightly.
The remaining healthy backends continue serving requests after the failed node is removed by the health checker.
Health checks continuously monitor backend availability. When a server becomes unreachable, it is removed from the routing pool and later restored automatically once healthy again.
The included control utility allows individual backends to be removed gracefully for testing or maintenance without restarting the load balancer.
Routing state is read on every request but updated infrequently by the health checker. Publishing immutable backend snapshots through atomic.Value allows request handlers to read routing information without lock contention while updates atomically replace the current snapshot.
The Go HTTP server already creates a goroutine for every incoming request. The buffered channel serves only as an admission semaphore that limits concurrent work, avoiding the additional scheduling complexity of a dedicated worker pool.
Opening a new TCP connection for every proxied request introduces unnecessary handshake overhead. Reusing persistent connections through http.Transport reduces latency and CPU utilization while improving throughput.
Make sure Go is installed. Developed and tested on:
$ go version
go version go1.26.0 darwin/arm64go run .go run testing/servers/servers.gogo run testing/flood/flood.goAll behavior is configured through a single JSON file.
{
"host": "localhost",
"port": "8080",
"backends": {
"api.localhost:8080": ["localhost:9000", "localhost:9001", "localhost:9002"],
"www.localhost:8080": ["localhost:9050", "localhost:9051", "localhost:9052"]
},
"tls": {
"enabled": false,
"certfile": "./cert.pem",
"keyfile": "./key.pem"
},
"timeouts": {
"readheader_timeout": 7,
"write_timeout": 7,
"client_timeout": 7
},
"max_queue": 100,
"max_idle_conns": 100,
"mode": 0
}| Field | Description |
|---|---|
backends |
Maps Host headers to backend server pools. |
max_queue |
Maximum number of concurrent in-flight requests admitted through the semaphore. |
max_idle_conns |
Idle connection budget per backend. |
tls.enabled |
Enables TLS termination. |
timeouts.* |
HTTP timeout configuration. |
mode |
Load balancing algorithm selector. |
| Mode | Algorithm | Description |
|---|---|---|
0 |
Atomic Round Robin | Sequential request distribution using an atomic counter. |
1 |
Atomic Least Connections | Routes requests to the backend with the fewest active connections. |
Gemini was used as a sounding board for discussing Go concurrency tradeoffs, validating implementation ideas, and debugging portions of the project. All architecture, implementation, benchmarking, and testing were completed by the project author.





