Technology 7 min read

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.

Caddy reverse proxy load balancing across multiple application servers
Architectural flow of Caddy distributing traffic across multi-node application pools.

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:

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:

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.

Sources and technical documentation