Gateway Pool: Enable Load Balancing
Load Balancing Mode in Detail
Activation
A route uses LB only when all three conditions are true:
route.target_pool_idis set (NOTtarget_peer_id)- Pool
mode = 'load_balancing' - Pool
lb_policy ∈ {round_robin, least_conn, ip_hash}
Caddy Configuration (HTTP)
During the build cycle, caddyConfig.resolveRouteUpstreams calls gatewayPool.resolveActivePeers(snapshot) — which returns all alive pool members:
"reverse_proxy": {
"upstreams": [
{ "dial": "10.8.0.2:18080" },
{ "dial": "10.8.0.8:18080" }
],
"load_balancing": {
"selection_policy": { "policy": "round_robin" }
},
"health_checks": {
"passive": {
"fail_duration": "30s",
"max_fails": 3,
"unhealthy_status": [500, 502, 503, 504]
}
}
}
LB Policies
| Policy | Behavior | When to use |
|---|---|---|
round_robin |
Round-robin through the upstream list | Symmetric load, identical members |
least_conn |
Member with fewest active connections | Long streams (Jellyfin streaming, backups) |
ip_hash |
Hash over client IP → fixed member | Session affinity without cookies (legacy apps) |
Trusted Proxies (for ip_hash behind a CDN/LB)
If GateControl sits behind a private LB or a CDN, the server Caddy only sees the proxy IP as the remote IP — ip_hash would then be ineffective (all requests hit the same bucket). The workaround is built into the srv0 HTTP server:
{
"trusted_proxies": {
"source": "static",
"ranges": [
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"100.64.0.0/10", "fd00::/8", "::1/128", "127.0.0.0/8"
]
},
"client_ip_headers": ["X-Forwarded-For"]
}
X-Forwarded-For is only honored when the connection originates from one of the listed ranges. Clients hitting GateControl directly from the internet cannot spoof XFF, because their source IP is not in the trust list.
Passive Health Checks
In addition to the gatewayHealth watchdog (every 30 s), Caddy itself checks passively:
- Sends a request to member X
- X returns a 5xx status (or fails to connect)
- After
max_fails=3such failures, X is removed from rotation forfail_duration=30s - After 30 s, retried
This makes circuit breaking kick in within seconds, instead of waiting for the global health logic's 90 s gateway_down_threshold.
L4 Load Balancing (TCP/UDP)
Works the same way for L4 routes — caddy-l4's proxy handler accepts multiple upstreams[] plus load_balancing.selection_policy:
{
"handler": "proxy",
"upstreams": [
{ "dial": ["10.8.0.2:13389"] },
{ "dial": ["10.8.0.8:13389"] }
],
"load_balancing": {
"selection_policy": { "policy": "round_robin" }
},
"health_checks": {
"passive": { "fail_duration": "30s", "max_fails": 3 }
}
}
L4 routes must have route_type='l4' and be pool-bound via target_pool_id.