HAProxy Load Balancing
A load balancer is a scheduler with a stale view of the world. This module builds the model you need when traffic is distributed but latency is not: what least-conn actually orders on, why a failing backend keeps serving for seconds after it broke, which of the two maxconn numbers queues rather than refuses, and what a stick table really keys on. Verified against HAProxy v3.4.6.
What you'll explore
- 01
Choosing a Backend
Every algorithm is a different answer to the question "which server is least busy?", and two of them refuse to answer it at all. HAProxy resolves the choice in assign_server() (src/backend.c:645), which switches on the lookup method rather than on the algorithm name: a tree lookup for roundrobin/leastconn/first, a hash for source/uri, a map or consistent-hash tree for static-rr and random (src/backend.c:719-729). The distinction that matters operationally is not round-robin versus least-conn — it is dynamic versus static. roundrobin, leastconn, first and consistent-hash all carry BE_LB_PROP_DYN (src/lb_fwlc.c:886, src/lb_fwrr.c:676, src/lb_chash.c:652); the map-based lookup used by default for source and uri hashing does not (src/lb_map.c:278). A static algorithm cannot reweight a server while it runs, which is why two other controls on this page quietly stop working when you pick one.
- 02
Deciding a Server Is Dead
A health check does not mark a server down. A run of them does. Each check result moves a counter, check->health, and the server only transitions to STOPPED when that counter has been spent — check_notify_failure() returns early on any non-zero health (src/check.c:589-590) and calls srv_set_stopped() only after it hits zero (src/check.c:593). So detection latency is not the interval you configured; it is the interval multiplied by the fall count, plus however long the failing check takes to time out. That is the number to compare against your deploy's drain window, and it is the reason a backend that broke ten seconds ago is still in rotation.
- 03
maxconn Is Not One Number
There are two limits with the same name and they fail differently. A frontend maxconn caps what HAProxy accepts; excess connections wait in the kernel's accept queue. A server maxconn caps what one backend is given, and excess requests are queued inside HAProxy by pendconn_add() (src/queue.c:567) rather than refused — a queue you can see, with a wait time you can measure. The server limit is also not necessarily the number you typed: when minconn is set, srv_dynamic_maxconn() (src/queue.c:104) scales it with backend load, max = MAX(minconn, beconn × maxconn ÷ fullconn) (src/queue.c:111-112), collapsing to the static maxconn once the backend reaches fullconn (src/queue.c:108-110). And what it counts is sessions, not sockets: the comparison is against s->cur_sess (include/haproxy/queue.h:81), "number of currently active sessions" (include/haproxy/server-t.h:438), incremented once per stream (src/backend.c:2296).
- 04
Remembering Where a Client Went
A stick table is not a connection tracker. It is a key-value store whose key is whatever sample expression the configuration names — stktable_fetch_key() (src/stick_table.c:1824) evaluates that expression and hands the result to stktable_get_entry() (src/stick_table.c:781), which looks the key up and creates the entry if it is absent. Key on src and you get per-client-address stickiness; key on a cookie or a header and you get stickiness that survives a client changing address. Entry lifetime is idle-based rather than absolute: a sticking rule touches the entry through stktable_touch_with_exp() (src/stick_table.c:630), which stores the new expiry and re-arms the table's timer (src/stick_table.c:636-639) — so an entry lives as long as the client keeps arriving. A full table is the second, clock-independent story, and it is not a simple eviction: stktable_trash_oldest() (src/stick_table.c:275) walks the expiry tree nearest-first (src/stick_table.c:313) and, on meeting an entry that has been touched since it was queued or is still referenced, repairs its position and moves on rather than deleting it (src/stick_table.c:342, src/stick_table.c:351-352). Active clients are therefore protected; what a too-small table costs you is the arrivals, because when nothing is purgeable stksess_new() returns NULL (src/stick_table.c:446-458) and the new client simply gets no entry.
- 05
Routing Before Balancing
Load balancing is the second decision. The first is which backend the request belongs to at all, and that is an ACL: acl_exec_cond() (src/acl.c:1044) evaluates a condition against the current sample, and rules reach it through the acl_match_cond() wrapper (include/haproxy/acl.h:113) that folds the pass/fail into the rule's own polarity. This ordering is why a content-switching rule changes the shape of your load distribution without touching the algorithm: once traffic is split by path or Host, each backend balances only its own share, and a hot prefix concentrates on the servers behind it. It is also where source-based routing and source-based balancing can disagree — the ACL sees the client address, the hash may see the same address, and the two are doing entirely different jobs with it.
Validated against HAProxy (load balancer / reverse proxy reference implementation) v3.4.6 — src/lb_fwlc.c and friends. Every simulated behavior cites a real source function, and a server-side correlation engine computes how each layer's state cascades into the next. See the methodology →
This module is part of zerohop Pro — it unlocks alongside the full kata library, progress tracking, and weak-area analysis.