Caddy Reverse Proxy Load Balancing: Setup Guide
Caddy reverse proxy load balancing spreads web traffic across backend servers while handling automatic HTTPS and quick node failover. This guide covers upstream routing rules, sticky cookies, active health probes, and circuit breaker settings for production systems.
How Caddy routes traffic across backend pools
Modern web infrastructure relies on multiple app instances. Having several copies helps maintain high uptime. When you list several backend targets under one rule, Caddy builds an upstream pool. The server manages these network links directly inside its Go runtime.
By default, the proxy picks a target at random. Random picks work fine for simple APIs. Production systems need strict control. You want busy servers to get fewer jobs. You also want dead servers removed from the pool right away.
Every upstream server inside a reverse_proxy rule gets tracked. If a backend stops answering, the edge router spots the problem. It routes fresh traffic to healthy instances instead. Once the bad node recovers, the proxy brings it back into service without any manual restart.
Check our setup guide for caddy reverse proxy docker compose to see how bridge networks protect container upstreams from public exposure.
Choosing the right load balancing policy
The gateway offers several balancing rules through the lb_policy subdirective. Each policy fits a distinct workload. Pick the rule that matches your architecture.
example.com {
reverse_proxy node1:8080 node2:8080 node3:8080 {
lb_policy least_conn
}
}
The code above tells the proxy to pick the server with the fewest active jobs. This stops latency spikes on slow database calls.
| Load Balancing Policy | Selection Mechanism | Primary Production Use Case | Statefulness |
|---|---|---|---|
round_robin |
Sequential rotation across healthy backends | Equal-capacity stateless microservices and static asset pools | Stateless |
least_conn |
Directs traffic to upstream with fewest active requests | Long-running queries, heavy computation, and variable workloads | Stateless |
first |
Routes all traffic to the first available backend in order | Active-passive standby failover and rolling maintenance deployments | Stateless |
ip_hash |
Hashes direct client IP to a sticky backend using HRW | Simple TCP proxying where clients do not sit behind secondary CDN layers | Sticky (IP) |
client_ip_hash |
Hashes original client IP from trusted proxy headers | Cloudflare or CDN setups with trusted_proxies enabled in global options | Sticky (IP) |
cookie |
Sets HMAC-SHA256 cookie pointing to upstream dial address | Legacy session-bound monolithic applications requiring sticky sessions | Sticky (Cookie) |
When using hash rules like ip_hash or uri_hash, the edge service runs Rendezvous hashing. This math trick limits server changes when a node drops offline. Most users stay pinned to their usual node.
For apps that need sticky user sessions, turn on cookie tracking with a secret key. This directive keeps returning users on the exact same upstream host.
example.com {
reverse_proxy srv1:3000 srv2:3000 srv3:3000 {
lb_policy cookie app_session "secret-session-salt-9921"
lb_try_duration 4s
}
}
When a new browser connects for the very first time, the balancer picks a backend target.
It returns a Set-Cookie header.
Future requests presenting this cookie automatically reach the identical application node.
See our guide on caddy reverse proxy subpath to learn how path routing works alongside load balancing.
Setting up active health checks
Waiting for real users to hit broken servers hurts your service. Active health checks solve this issue. The server sends recurring test pings to your backends in the background. It cuts dead nodes before visitors notice errors.
example.com {
reverse_proxy node1:8080 node2:8080 node3:8080 {
lb_policy round_robin
# Active background health checks
health_uri /healthz
health_interval 5s
health_timeout 2s
health_status 2xx
}
}
This snippet polls the /healthz path every five seconds. If a node takes over two seconds to answer, the proxy drops it from the rotation. The engine keeps it out until probe checks pass again.
Read our breakdown of mtls vs tls difference to see how client certificates protect private probe endpoints.
Passive health checks and circuit breaker rules
Active checks poll servers on a timer. But an app can crash between checks. Passive checks inspect real user requests in real time. They act like a circuit breaker, catching crashes as they happen.
example.com {
reverse_proxy app1:8080 app2:8080 app3:8080 {
lb_policy least_conn
# Passive circuit breaker configuration
fail_duration 30s
max_fails 3
unhealthy_status 502 503 504
# Request retry and failover controls
lb_try_duration 5s
lb_try_interval 250ms
}
}
These settings watch for errors using clear rules:
fail_duration 30s: Tells the edge proxy how long to remember server errors. A value above zero turns on passive checks.max_fails 3: The count of bad responses needed to mark a node down.unhealthy_status 502 503 504: Marks these HTTP status codes as failures. Normal 404 client errors do not count.lb_try_duration 5s: Tells the gateway to re-send failed requests to other nodes for five seconds before showing an error.
Using both check types gives the best protection. Active checks handle clean recovery. Passive checks catch sudden node crashes.
Review our protocol test of tls 1.3 vs tls 1.2 to see how modern handshakes speed up proxy reconnections.
Common production mistakes to avoid
Running multi-server proxy setups requires careful tuning. Watch out for these common engineering traps:
- The Zero Interval CPU Loop: Do not set
lb_try_interval 0with retries turned on. If all nodes go down, the loop runs fast and burns CPU. Keep a safe gap like250ms. - Dynamic Upstream Gaps: Dynamic upstreams like DNS SRV records change often. The proxy cannot run active health checks against them. Use passive checks for dynamic setups.
- Missing Proxy Headers: The gateway sets standard forwarding headers like
X-Forwarded-For. Make sure your web app trusts them so redirect links stay accurate. - WebSocket Session Drops: WebSockets need a steady link. If your app keeps state in memory, use sticky cookies or IP hashing so connections do not jump between servers.
Frequently asked questions
What is the default load balancing policy in Caddy?
The proxy uses random selection as its default load balancing policy. You can change it with lb_policy inside the reverse_proxy block.
How does Caddy handle sticky sessions across backends?
The proxy supports cookie-based stickiness. It computes a hash of the chosen backend dial address and stores it in a client response cookie.
Why are active health checks disabled for dynamic upstreams?
Dynamic upstreams discover targets on the fly. The proxy cannot run fixed background timers against fluid target lists, relying instead on passive checks.