Sticky sessions pin a client to a specific backend server for the duration of a logical session. Sometimes this is necessary (stateful uploads, WebSocket reconnects to the same node, legacy apps without a shared session store); often it is a workaround for a missing shared state layer. This article covers the three sticky mechanisms HAProxy supports and the production caveats for each.
How to verify
For an existing config, the runtime API exposes the active stick tables and the cookies HAProxy is inserting.
echo "show table be_app" | sudo socat /run/haproxy/admin.sock -
echo "show servers state" | sudo socat /run/haproxy/admin.sock -
curl -v http://localhost/ 2>&1 | grep -i 'set-cookie'
curl -v -b "SRVID=app1" http://localhost/ 2>&1 | grep -i 'x-served-by\|cookie'
The show table output lists every key the table currently holds (source IP, cookie value) with the server it points at. The verbose curl confirms whether HAProxy is inserting the cookie and which server it routed the client to.
What’s happening
HAProxy supports three approaches:
cookiedirective (HTTP-only) — HAProxy inserts or rewrites a cookie identifying the backend server. Subsequent requests from the same client carry the cookie and HAProxy routes accordingly.stick-table+stick on(HTTP or TCP) — HAProxy maintains an in-memory table keyed by some request attribute (source IP, cookie value, header value) that maps to a server. Survives across multiple requests from the same key.balance source(TCP or HTTP) — a hash of the client IP picks the server. No state; rebalances naturally when servers join or leave.
cookie is the production default for HTTP because it survives client IP changes (mobile networks, NAT) and is explicit — you can see the cookie in DevTools. stick-table is what you use when you cannot insert a cookie (TCP frontends, third-party clients that strip cookies). balance source is the fallback when you have neither.
The big trade-off: stickiness pins a client to a server, which means a sick server keeps receiving requests from its pinned clients until it goes DOWN. The redispatch option exists precisely for this; without it, a sticky session attached to a dead server returns errors forever.
The procedure
-
Cookie-based stickiness. Insert a cookie naming the server, scope it to the path, and let HAProxy rewrite the server selection:
backend be_app balance roundrobin cookie SRVID insert indirect nocache httponly secure option httpchk http-check send meth GET uri /healthz http-check expect status 200 server app1 10.0.1.11:8080 check cookie app1 server app2 10.0.1.12:8080 check cookie app2The
cookie app1on the server line is the value HAProxy writes into theSRVIDcookie when this server is chosen. Theinsert indirect nocache httponly secureflags are the production combination: insert means HAProxy adds the cookie, indirect means HAProxy strips it from the request before forwarding (so the backend doesn’t see it), nocache makes it not survive in caches, httponly prevents JS access, secure requires HTTPS. -
Stick-table by source IP. For TCP frontends or when you cannot insert cookies:
backend be_app balance leastconn stick-table type ip size 100k expire 30m stick on src server app1 10.0.1.11:8080 check server app2 10.0.1.12:8080 checksize 100kis the maximum number of entries;expire 30mis the idle timeout per entry. The table lives in memory only — see HAProxy stick tables for persistence across reloads and HAProxy peer protocol for replicating across instances. -
Stick on a header value. When the client identity is in a custom header:
backend be_app stick-table type string len 64 size 50k expire 1h stick on hdr(x-tenant-id) server app1 10.0.1.11:8080 check server app2 10.0.1.12:8080 checktype string len 64allocates 64 bytes per key; sizing matters because the table is preallocated. -
balance source — no state needed. A hash of the source IP picks the server:
backend be_app balance source hash-type consistent server app1 10.0.1.11:8080 check server app2 10.0.1.12:8080 checkhash-type consistentuses a Karger-style ring so adding or removing a server reshuffles only ~1/N of the clients. Withoutconsistent, every client gets reshuffled when the pool changes. -
Combine cookie and redispatch. The production-safe pattern: stick clients to a server, but redispatch if that server is down:
defaults option redispatch retries 3 backend be_app cookie SRVID insert indirect nocache httponly secure option httpchk http-check send meth GET uri /healthz http-check expect status 200 server app1 10.0.1.11:8080 check cookie app1 server app2 10.0.1.12:8080 check cookie app2option redispatchis what saves you when a stuck client tries to reach a downed pin: HAProxy gives up after the retries and routes to a healthy server. -
Persist stick-table across reloads. Without persistence, a
systemctl reload haproxyempties the table:backend be_app stick-table type ip size 100k expire 30m peers cluster_peers stick on src server app1 10.0.1.11:8080 check server app2 10.0.1.12:8080 checkThe
peers cluster_peersclause replicates the table to peers. On reload, HAProxy pulls state from a peer.
Common pitfalls
- A sticky session without
option redispatchkeeps trying the dead server forever; the client sees errors until the cookie expires. - Cookie-based stickiness with
insertrewrites theSet-Cookieheader — if the backend already setsSRVIDfor its own purposes, you collide. Use a unique cookie name. balance sourcewithouthash-type consistentreshuffles every client on a server swap. With autoscaling, this is a thundering herd against cold caches.- Stick tables are per-process — without
peers, a multi-instance HAProxy cluster has N independent tables. The client whose state lives on instance A but who hits instance B in the next request gets reshuffled. - The cookie HAProxy inserts is HTTP-only and Secure by best practice, but if the app explicitly reads its own session cookie and HAProxy is also inserting one, your debug logs will fill with the wrong cookie. Use distinct names.
Stack Harbor recommends moving to a shared session store (Redis, database, JWT) as the first option and treating sticky sessions as the fallback for legacy apps. When sticky is required, we deploy with peers replication so a reload or instance loss does not unstick everyone at once. That continuity is part of how we run Clustered Environments.