Load Balance Django With HAProxy

Put HAProxy in front of several Django app servers: active health checks on a /health/ endpoint, TLS termination, X-Forwarded-Proto, connection limits per server, rolling deploys with drain, and stick tables for basic rate limiting.

Problem statement

Once Django runs on two or more app servers, something must spread traffic across them, notice when one fails, and let you take servers out for deploys without errors. Cloud load balancers do this for a fee; on your own servers, HAProxy is the standard open-source choice. It offers active health checks (which open-source Nginx lacks), detailed stats, per-server connection limits, and runtime control for draining servers during rolling deploys.

HAProxy in front of DjangoClientsHTTPSHAProxyTLS, health checksApp serversNginx or GunicornDrain + deployone server at a timeHAProxy checks each backend and routes only to healthy ones, which makes rolling deploys safe.
  1. Install HAProxy on a dedicated host (or pair with keepalived for redundancy).
  2. Define a frontend that terminates TLS and sets X-Forwarded-Proto, and a backend with each app server.
  3. Add active health checks: option httpchk with http-check send meth GET uri /health/ hdr Host example.com.
  4. Limit concurrency per server with maxconn close to each server's worker capacity.
  5. For deploys, set a server to drain via the runtime API, deploy, then set it back to ready.
  6. Trust X-Forwarded-For and X-Forwarded-Proto from HAProxy only, in Nginx or Django.

Step-by-step solution

1. Basic configuration

# /etc/haproxy/haproxy.cfg
global
    log /dev/log local0
    maxconn 4096
    stats socket /run/haproxy/admin.sock mode 660 level admin

defaults
    mode http
    log global
    option httplog
    option forwardfor
    timeout connect 5s
    timeout client  60s
    timeout server  60s

frontend https_in
    bind :80
    bind :443 ssl crt /etc/haproxy/certs/example.com.pem alpn h2,http/1.1
    http-request redirect scheme https unless { ssl_fc }
    http-request set-header X-Forwarded-Proto https if { ssl_fc }
    default_backend django

backend django
    balance leastconn
    option httpchk
    http-check send meth GET uri /health/ ver HTTP/1.1 hdr Host example.com
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 maxconn 40
    server app1 10.0.0.11:80 check
    server app2 10.0.0.12:80 check

leastconn suits Django well because request durations vary. option forwardfor adds X-Forwarded-For.

2. Health checks Django can pass

The check sends Host: example.com, so it passes ALLOWED_HOSTS, and uses plain HTTP to the backend, so exempt /health/ from SECURE_SSL_REDIRECT or rely on Nginx on each app server answering it. A server is marked down after 3 failures (fall 3) and up after 2 successes (rise 2), checked every 3 seconds.

inter 3s, fall 3, rise 2app2 stops answeringt = 0 sThree failed checkst = 9 sapp2 marked DOWNtraffic to app1 onlyTwo good checksafter recoveryapp2 marked UPtraffic resumesActive checks detect failure without waiting for user requests to faildjango-deployment.com
Tune inter, fall, and rise for how quickly you want failures detected.

3. Concurrency limits

maxconn 40 on each server makes HAProxy queue requests at the load balancer instead of overloading a backend. Set it near the backend's real capacity: Gunicorn workers times threads, plus a little headroom. Queued requests wait up to timeout queue (defaults to timeout connect); set it explicitly, for example timeout queue 30s.

4. Rolling deploys with drain

echo "set server django/app1 state drain" | sudo socat stdio /run/haproxy/admin.sock
# wait for active sessions to reach zero
echo "show servers state django" | sudo socat stdio /run/haproxy/admin.sock
# deploy app1, run checks
echo "set server django/app1 state ready" | sudo socat stdio /run/haproxy/admin.sock

drain stops new connections while existing ones finish. Script this per server in your deploy tool so only one server is out at a time.

5. Forwarded headers in Django

If HAProxy talks directly to Gunicorn, configure Django:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

and restrict the app servers' firewall so only HAProxy can reach them, which keeps those headers trustworthy. If Nginx runs on each app server, use set_real_ip_from <haproxy-ip>; and real_ip_header X-Forwarded-For; there, and pass X-Forwarded-Proto through instead of overwriting it with $scheme.

6. Basic rate limiting

frontend https_in
    stick-table type ip size 100k expire 60s store http_req_rate(10s)
    http-request track-sc0 src
    http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }

This rejects clients making more than 100 requests in 10 seconds.

HAProxy for DjangoActive health check on /health/with a valid Host headerX-Forwarded-Proto set on TLSDjango trusts itmaxconn per server near capacityqueue at the balancerDrain before each server deployruntime APIApp servers reachable only from HAProxyfirewall ruleThese turn a list of servers into a resilient pooldjango-deployment.com
A single HAProxy is itself a single point of failure; pair two with keepalived for HA.

Verification check: the stats page or show stat shows both servers UP; stopping Gunicorn on one server marks it DOWN within about 10 seconds while the site stays up; a drained deploy produces no 5xx responses in HAProxy logs.

Explanation

HAProxy is a high-performance TCP and HTTP load balancer. Unlike passive failure detection, its active checks probe each backend on a schedule and change server state independently of user traffic, so a broken server is removed before users hit it. Its queueing model (maxconn per server with a global queue) protects Django servers from overload by holding excess requests at the balancer, and the runtime API makes controlled deploys possible without editing configuration.

Edge cases / notes

  • WebSockets work through HAProxy in HTTP mode; set timeout tunnel 1h for long-lived connections.
  • Sticky sessions are rarely needed for Django when sessions live in the database or Redis; avoid them unless required.
  • Certificates. HAProxy expects certificate and key in one PEM file; reload after renewals with a Certbot deploy hook.
  • HTTP/2 to clients is supported with alpn h2,http/1.1; backends stay HTTP/1.1.

Next steps

FAQ

HAProxy or Nginx for load balancing Django?

Both work. HAProxy has active health checks and runtime control in the open-source version; Nginx needs NGINX Plus for active checks.

How does HAProxy know a Django server is healthy?

Through active HTTP checks to a health endpoint, marking servers down after consecutive failures.

How do I deploy without downtime behind HAProxy?

Drain one server through the runtime API, deploy it, set it ready, then repeat for the next.

Do I need sticky sessions?

Usually not. Store sessions in the database or Redis so any server can handle any request.

Keep going