HyperText Transfer Protocol (HTTP), the protocol that defines the language browsers use to communicate with web servers, is stateless, meaning that after you make a web request and a server sends back a response, no memory of that interaction remains. Websites need other ways to remember previous user interactions to make anything more sophisticated than a static web page work.
These days, Javascript frameworks like Vue.js, React.js, and others let developers create single-page applications (SPAs) that provide statefulness to the otherwise stateless web. Because they’re implemented as Javascript applications running in the user’s browser, they can keep track of what the user has done and render the app in a way that accounts for that shared history. However, the user’s browser is not a secure location for storing confidential information or data that the user shouldn’t have control of.
When they need secure storage, developers turn to save data on the web server. On the server, developers can use session storage, an in-memory cache for storing data that can be accessed quickly. It’s the ideal place for saving temporary user information, such as whether the user is currently logged in.
Session storage is typically kept in the runtime memory of the server and, as a consequence of that, requires each user to connect to the same server where their particular session was created. The difficulty arises when you add load balancing into the mix, which, by definition, aims to split up requests across many backend servers, unaware of the fact that a person’s session is stored on only one of them.
How Load Balancing Affects Session Data
A load balancer allows a website to scale out its capacity to handle more users by dispersing requests across a group of servers that all share the work. However, if your session was created on Server A, but your next request is load balanced to Server B, then Server B will have no idea who you are. It will need to create a new session for you, which will lose your current state, requiring you to log in again, for example.
A load balancer wants to split up a user’s requests across different servers, but that user’s session exists on only one of them. What’s the solution?
In this article, I’ll show you how to enable sticky sessions in your HAProxy load balancer, which helps HAProxy to always send a user back to the same server where their session is stored. However, before I do, I’ll give a disclaimer: while sticky sessions solve the problem because it forces HAProy to keep sending a user to the same server, it is at odds with the goal of load balancing. Other, more scalable solutions exist.
I recommend storing sessions in a database that’s accessible to all servers. Databases like Redis excel at this, and many server-side web frameworks such as Express.js support it (check out the compatible databases for express-session). That way, no matter which web server the load balancer sends a user to, that server can fetch their session. You can then go back to load balancing across all servers normally.
However, sometimes people require sticky sessions for one reason or another, and it’s good to know that HAProxy supports it.
Do you use HAProxy Enterprise? Click here to learn about sticky sessions in HAProxy Enterprise.
How to Implement Sticky Sessions in HAProxy?
#1 Implement sticky sessions with a cookie
HAProxy can save a cookie in the user’s browser to remember which server to send them back to. The cookie, which contains the server’s unique ID, offers the most accurate way to implement sticky sessions because a cookie is guaranteed to belong to only one user.
Edit your HAProxy configuration file, /etc/haproxy/haproxy.cfg. To create the cookie in the user’s browser, add a cookie
directive to the backend section that contains your web servers and adds a cookie parameter to each server line to set the cookie’s value:
frontend mywebapp | |
bind :80 | |
mode http | |
default_backend webservers | |
backend webservers | |
mode http | |
balance roundrobin | |
cookie SERVER insert indirect nocache | |
server web1 192.168.56.20:80 check cookie web1 | |
server web2 192.168.56.21:80 check cookie web2 |
If the user was first relayed to the web1 server, then their cookie will contain the value web1, and HAProxy will know to keep sending them there. The insert
parameter creates the cookie, indirect
removes the cookie on each incoming request before forwarding the message to the server, and nocache
sets the Cache-Control: private HTTP header so that cache servers between HAProxy and the user won’t cache the response.
If your list of servers is generated dynamically, for example, through DNS service discovery, then you can use the dynamic
parameter to indicate that the cookie’s value should be a combination of the server’s IP address, port, and a secret key set with dynamic-cookie-key
. In the example below, we’re leveraging DNS service discovery and generating the cookie’s value dynamically:
frontend mywebapp | |
bind :80 | |
mode http | |
default_backend webservers | |
resolvers mydns | |
nameserver dns1 192.168.56.30:53 | |
accepted_payload_size 8192 | |
backend webservers | |
mode http | |
balance roundrobin | |
cookie SERVER insert indirect nocache dynamic | |
dynamic-cookie-key mysecretphrase | |
server-template web 10 myservice.example.local:80 check resolvers mydns init-addr libc,none |
#2 Implement sticky sessions with the client’s IP
While cookies offer the most accurate way to match a user with the server that has their session, HAProxy can also leverage the user’s IP address for this purpose. This method works for non-HTTP applications too.
Beware that IP addresses aren’t always unique to a single user, particularly when network address translation (NAT) is in use, such as for users originating from behind a corporate proxy or when an ISP allocates a pool of public IP addresses for customers. In practice, this means that more users might be routed to the same server, causing the load to be distributed more unevenly. Also, a user’s IP address can change, such as when DHCP leases them a new one, in which case their session would be lost. Even so, IP addresses provide a good-enough option for sticking users to a server when a cookie isn’t an option.
Start by creating a peers
section containing a stick table that stores user IP addresses.
Then, in your backend section, use the stick match
and stick store-request
directives to save the user’s source IP address in the table and match it with the server they used.
Add a similar to the following to your /etc/haproxy/haproxy.cfg file:
frontend mywebapp | |
bind :80 | |
mode http | |
default_backend webservers | |
peers sticktables | |
bind :10000 | |
# On the next line, 'loadbalancer1' | |
# is the HAProxy server's hostname | |
server loadbalancer1 | |
table sticky-sessions type ip size 1m | |
backend webservers | |
mode http | |
balance roundrobin | |
stick match src table sticktables/sticky-sessions | |
stick store-request src table sticktables/sticky-sessions | |
server web1 192.168.56.20:80 check | |
server web2 192.168.56.21:80 check |
You can use a peers
section like this to declare the stick table, like we’ve done here, even when you manage only one HAProxy instance, but it really comes in handy when you manage multiple HAProxy instances in active-active or active-standby mode. You can add more server
lines to the peers
section to share the stick table storage with the other HAProxy instances so that the other load balancers will have the server-matching data stored too.
If you’ve explored the HAProxy configuration documentation in depth, you may have come across the directive balance source
, which also offers a way to stick a user to the first server they used. However, it determines which server to use by dividing the weight of all servers—weight is a parameter you can assign to servers to send more or less traffic to them—and then choosing a server based on a hash of the user’s IP address. This method, which relies on the total weight of all existing servers, can kick out many users if even one server fails, redistributing users with the recalculated hashes. Stick tables generally present a more stable option since they will dispatch only the users using the failed server.
Sticky Sessions in HAProxy: Conclusion
In this blog post, you learned how HAProxy supports sticky sessions. You can implement them with a cookie or the user’s IP address. While sticky sessions are sometimes necessary, other, more scalable solutions exist, such as storing session data on a separate database.
Interested to know when we publish content like this? Subscribe to our blog.
To learn more about stick tables, sign up for the on-demand webinar, Introduction to HAProxy Stick Tables.
Subscribe to our blog. Get the latest release updates, tutorials, and deep-dives from HAProxy experts.