A lightweight, robust, and zero-dependency HTTP reverse proxy and load balancer implemented from scratch in Go using only standard library primitives (net/http, net/url, net, sync, time).
revprox demonstrates how core reverse proxy and load balancing mechanics—such as atomic round-robin routing, active background health checking, RFC-compliant hop-by-hop header filtering, X-Forwarded-* header handling, real-time SSE streaming, sync.Pool buffer recycling, and graceful shutdown—work under the hood without relying on black-box frameworks or httputil.ReverseProxy.
flowchart LR
Client([Client Request]) --> Proxy["revprox (:3000)"]
subgraph Pool ["ServerPool Load Balancer"]
direction TB
HC["Health Checker<br/>(Background Probe)"]
RR{"Round-Robin<br/>Selector"}
HC -.->|Syncs Health| RR
end
Proxy --> RR
RR -->|Forward| B1["Backend 1 (:9001)<br/>Status: Online"]
RR -.->|Bypass| B2["Backend 2 (:9002)<br/>Status: Offline"]
RR -->|Forward| B3["Backend 3 (:9003)<br/>Status: Online"]
- Zero External Dependencies: Built entirely with Go's standard library.
- Round-Robin Load Balancing:
- Thread-safe, lock-free round-robin peer selection powered by
sync/atomic. - Automatically skips offline backends and routes traffic exclusively to healthy nodes.
- Returns
503 Service Unavailableif all backends are down.
- Thread-safe, lock-free round-robin peer selection powered by
- Active Background Health Checking:
- Periodically probes all configured upstreams using lightweight TCP handshakes (
net.DialTimeout). - Thread-safe status updates using
sync.RWMutex. - Immediate startup probe: Detects offline nodes before the very first client request arrives.
- Smart logging: Only logs state changes (
is now DOWN ❌/is now healthy) to prevent console spam.
- Periodically probes all configured upstreams using lightweight TCP handshakes (
- Hop-by-Hop Header Sanitization (RFC 7230 §6.1):
- Dynamically extracts and removes headers specified inside the client's or upstream's
Connectionheader before strippingConnectionitself. - Removes default hop-by-hop headers:
Transfer-EncodingUpgradeProxy-AuthorizationTrailerTeProxy-AuthenticateKeep-AliveConnection
- Sanitizes both outbound requests to the upstream server and incoming responses forwarded back to the client.
- Dynamically extracts and removes headers specified inside the client's or upstream's
- Header Forwarding (
X-Forwarded-*):X-Forwarded-For: Appends the client IP (extracted cleanly usingnet.SplitHostPort), preserving any existing proxy chains.X-Forwarded-Host: Preserves the originalHostsent by the client.X-Forwarded-Proto: Indicates protocol scheme (httporhttpsdepending on client TLS state).
- Streaming & SSE (Server-Sent Events) Support:
- Immediate chunk flushing with
http.Flusherfor unbuffered event delivery. - Clears write deadlines via
http.ResponseController.SetWriteDeadline(time.Time{})so long-lived streams are not killed by server timeouts. - Early client disconnect detection (
broken pipe) aborts upstream reading immediately. - Memory recycling: Uses
sync.Pool(bufferPool) to recycle 32 KB byte buffers, minimizing garbage collection churn.
- Immediate chunk flushing with
- Graceful Shutdown:
- Captures
SIGINT(Ctrl+C) andSIGTERMviasignal.NotifyContext. - Drains in-flight requests cleanly within a 10-second grace window using
server.Shutdown(shutCtx).
- Captures
- Server & Transport Hardening:
- Custom
http.Transportwith a 5-secondResponseHeaderTimeout. - HTTP Server timeouts configured to prevent slow-client resource exhaustion and Slowloris attacks:
ReadHeaderTimeout: 3 secondsReadTimeout: 10 secondsWriteTimeout: 15 secondsIdleTimeout: 60 seconds
- Custom
- Gateway Error Handling:
- Distinguishes network timeouts (
504 Gateway Timeout) from connection refusals (502 Bad Gateway).
- Distinguishes network timeouts (
- Built-in Mock Backend:
- Companion test backend in
server/with/api/ping,/api/echo, and/api/stream(SSE) endpoints.
- Companion test backend in
revprox/
├── main.go # Core reverse proxy, load balancer, and health checker
├── go.mod # Go module declaration (github.com/boldbug1/revprox)
├── server/ # Sample backend service for local testing
│ ├── main.go # Backend test server with /api/ping, /api/echo, /api/stream
│ └── go.mod # Backend module declaration (temp-server)
└── README.md # Project documentation
- Go 1.22+ (tested with Go 1.26+)
Start the companion test backend on port 9001:
cd server
go run main.goOutput:
Starting hidden backend on http://127.0.0.1:9001
In a separate terminal, launch revprox from the root directory:
go run main.goOutput:
Registered backend: http://127.0.0.1:9001/api/
Registered backend: http://127.0.0.1:9002/api/
Registered backend: http://127.0.0.1:9003/api/
[HealthCheck] status changed: http://127.0.0.1:9002/api/ is now DOWN ❌
[HealthCheck] status changed: http://127.0.0.1:9003/api/ is now DOWN ❌
proxy listening on :3000
Note
revprox automatically discovers that 9002 and 9003 are offline during startup and directs 100% of traffic to healthy node 9001.
Send requests to revprox at http://localhost:3000/ping:
curl -i http://localhost:3000/pingExpected Response:
HTTP/1.1 200 OK
Content-Type: application/json
Date: ...
Content-Length: 18
"{message: pong}"Send custom hop-by-hop and client headers:
curl -i -X POST http://localhost:3000/echo \
-H "Connection: close, X-Custom-Hop" \
-H "X-Custom-Hop: should-be-stripped" \
-H "Keep-Alive: timeout=10" \
-d "hello reverse proxy"Observe that:
X-Custom-Hop,Keep-Alive, andConnectionare stripped before reaching upstream.X-Forwarded-For,X-Forwarded-Host, andX-Forwarded-Protoare automatically attached.- The upstream server's own
Keep-Alive: timeout=5header is stripped before sending the response back to your client.
Test real-time unbuffered event streaming through the proxy:
curl -N http://localhost:3000/streamObserve that:
- Events stream immediately every 1 second without buffering delays.
- Terminating curl (
Ctrl+C) immediately propagates cancellation upstream to terminate the backend stream goroutine. http.ResponseControllerclears the write deadline, preventing the 15-secondWriteTimeoutfrom killing the connection.
- Start another backend instance on port
9002. - Within 5 seconds,
revproxlogs:[HealthCheck] status changed: http://127.0.0.1:9002/api/ is now healthy - Send multiple curl requests:
revproxwill alternate requests between:9001and:9002. - Kill the server on
:9001:revproxflags it asDOWN ❌and seamlessly routes all traffic to:9002without downtime.
Default configurations can be adjusted in main.go:
| Variable / Setting | Default Value | Description |
|---|---|---|
port |
:3000 |
Port revprox listens on |
backendURLs |
9001, 9002, 9003 |
List of upstream backend URLs |
| Health Check Interval | 5 * time.Second |
Frequency of background health probes |
| Health Check Timeout | 2 * time.Second |
Max wait time for TCP handshake per backend |
ResponseHeaderTimeout |
5 * time.Second |
Max duration waiting for upstream response headers |
ReadHeaderTimeout |
3 * time.Second |
Max time allowed to read client request headers |
ReadTimeout |
10 * time.Second |
Max time to read the entire request |
WriteTimeout |
15 * time.Second |
Max time to write standard response |
IdleTimeout |
60 * time.Second |
Max time to keep idle connections open |
| Shutdown Drain Timeout | 10 * time.Second |
Maximum window to drain in-flight requests on exit |
This project is licensed under the MIT License. Feel free to use, modify, and distribute.