Sticky sessions and rate limiting are often implemented together, but they solve fundamentally different problems. Sticky sessions ensure a user’s requests always go to the same backend server, usually for stateful applications. Rate limiting, on the other hand, protects your application from overload by restricting the number of requests a client can make within a given time period. HAProxy’s stick tables are a powerful tool for implementing both of these features efficiently, without requiring complex application-level logic or expensive external databases.
Let’s see how HAProxy’s stick tables can be used to achieve both.
Sticky Sessions with HAProxy Stick Tables
Imagine you have a web application that stores user session data on individual backend servers. If a user’s request is load-balanced to server A, and their next request goes to server B, server B won’t have the session data and the user will effectively be logged out or lose their progress. Sticky sessions solve this by ensuring all requests from a given client IP address (or other identifier) are consistently directed to the same backend server.
Here’s a simplified HAProxy configuration demonstrating sticky sessions using stick tables:
frontend http_frontend
bind *:80
mode http
default_backend web_servers
# Define the stick table for tracking client IPs
stick-table type ip size 100000 expire 30m store gpc0
# Use the stick table to achieve session stickiness
acl client_is_stuck src,table(client_ip_stick_table)
http-request set-map if client_is_stuck client_ip_stick_table:1
http-request use-backend web_servers if client_is_stuck
http-request capture request header X-Forwarded-For len 64
http-request set-map client_ip_stick_table src table_key capture(X-Forwarded-For) if capture.len gt 0
http-request set-map client_ip_stick_table src table_key
http-request redirect location / if !client_is_stuck
backend web_servers
mode http
balance roundrobin
server web1 192.168.1.10:80 check
server web2 192.168.1.11:80 check
server web3 192.168.1.12:80 check
Explanation:
stick-table type ip size 100000 expire 30m store gpc0: This defines a stick table namedclient_ip_stick_table.type ip: The key for the table will be the client’s IP address.size 100000: The table can store up to 100,000 entries.expire 30m: Entries older than 30 minutes will be automatically removed.store gpc0: This enables a counter (gpc0) for each entry, which we’ll use later for rate limiting.
acl client_is_stuck src,table(client_ip_stick_table): This creates an Access Control List (ACL) that checks if the client’s source IP (src) exists in theclient_ip_stick_table.http-request set-map if client_is_stuck client_ip_stick_table:1: If the client is already "stuck" (meaning their IP is in the table), this line sets a map entry for that IP. While not strictly necessary for basic stickiness, it’s often used in conjunction with more complex scenarios or for tracking.http-request use-backend web_servers if client_is_stuck: If the client is stuck, HAProxy will try to use theweb_serversbackend. This is where the actual routing happens.http-request capture request header X-Forwarded-For len 64: This captures theX-Forwarded-Forheader, which is important if HAProxy is behind another proxy or load balancer, to get the original client IP.http-request set-map client_ip_stick_table src table_key capture(X-Forwarded-For) if capture.len gt 0: IfX-Forwarded-Forwas captured, use that IP as the key in the stick table.http-request set-map client_ip_stick_table src table_key: IfX-Forwarded-Forwas not captured, use the direct source IP as the key. This populates the stick table with the client’s IP on their first request.http-request redirect location / if !client_is_stuck: This is a fallback. If the client isn’t "stuck" (i.e., their IP isn’t in the table yet), they are redirected to the root path. This is a simplified way to ensure the first request hits a server and gets added to the stick table. In a real-world scenario, you’d likely want to allow the first request to proceed to the backend without redirection, and theset-mapwould handle adding it.
The magic here is that HAProxy, when it sees a request from an IP already in the client_ip_stick_table, will attempt to send it to the same backend server that previously handled a request from that IP. This isn’t a hard-coded mapping; HAProxy’s internal logic favors servers that have recently served traffic from that client IP.
Rate Limiting with HAProxy Stick Tables
Now, let’s layer rate limiting on top. We want to prevent any single IP address from overwhelming our servers with too many requests in a short period. We can use the same stick table (or a separate one) to track request counts per IP.
Here’s how to extend the previous configuration to include rate limiting:
frontend http_frontend
bind *:80
mode http
default_backend web_servers
# Define the stick table for tracking client IPs and request counts
stick-table type ip size 100000 expire 1m store count
# Rate limiting ACL: check if requests exceed 10 per second
acl rate_limited src,table(rate_limit_table) ge 10
# If rate limited, deny the request
http-request deny if rate_limited
# For non-rate-limited requests, update the stick table and proceed
http-request track-sc0 src table(rate_limit_table)
http-request use-backend web_servers
backend web_servers
mode http
balance roundrobin
server web1 192.168.1.10:80 check
server web2 192.168.1.11:80 check
server web3 192.168.1.12:80 check
Explanation:
stick-table type ip size 100000 expire 1m store count: This defines a new stick table namedrate_limit_table.type ip: Key is the client IP.size 100000: Max entries.expire 1m: Entries expire after 1 minute. This means the rate limit is effectively "per minute". If you want "per second", you’d useexpire 1s.store count: This is crucial. It stores a counter for each IP address.
acl rate_limited src,table(rate_limit_table) ge 10: This ACL checks if the count for the client’s IP address inrate_limit_tableis greater than or equal to (ge) 10.http-request deny if rate_limited: If therate_limitedACL is true, HAProxy will deny the request immediately, returning a429 Too Many Requests(or similar, depending on HAProxy version and configuration) response.http-request track-sc0 src table(rate_limit_table): This is the core of the rate limiting. For every incoming request from a source IP (src), HAProxy increments the counter associated with that IP in therate_limit_table.track-sc0refers to the first counter (sc0) which is implicitly used whenstore countis specified.http-request use-backend web_servers: If the request is not rate-limited, it proceeds to be handled by the backend servers.
Combining Sticky Sessions and Rate Limiting:
You can combine these concepts. You might want to use a single stick table if the expiration times and storage requirements are compatible. For instance, if you want sticky sessions to last 30 minutes and rate limiting to be calculated over 1 minute, you’d likely need two separate stick tables or a more complex single table setup.
A common approach is to have one stick table for session stickiness (longer expiry, perhaps storing backend server affinity) and another for rate limiting (shorter expiry, storing request counts).
Here’s a more consolidated example, using two distinct stick tables for clarity:
global
# ... other global settings ...
nbproc 1
# Enable stick table statistics
stats socket /var/run/haproxy.sock mode 660 level admin
frontend http_frontend
bind *:80
mode http
default_backend web_servers
# Stick table for session persistence (e.g., 30 minutes)
stick-table type ip size 100000 expire 30m store gpc0 as session_stick
# Stick table for rate limiting (e.g., 1 minute)
stick-table type ip size 100000 expire 1m store count as rate_limit
# --- Session Stickiness Logic ---
acl client_is_stuck src,table(session_stick)
# If client is stuck, ensure they go to the same backend
# This is a simplified example; real stickiness is more nuanced
http-request set-map session_stick src table_key
http-request use-backend web_servers if client_is_stuck
# --- Rate Limiting Logic ---
acl rate_limited src,table(rate_limit) ge 10
http-request deny if rate_limited
# For all allowed requests, track them in the rate limit table
http-request track-sc0 src table(rate_limit)
# If not stuck, allow the request to proceed to the backend
# The 'set-map' above will add it to the session_stick table
# if it's a new client.
http-request use-backend web_servers if !client_is_stuck
backend web_servers
mode http
balance roundrobin
server web1 192.168.1.10:80 check
server web2 192.168.1.11:80 check
server web3 192.168.1.12:80 check
In this combined example, session_stick handles persistence, and rate_limit enforces the request cap. The http-request use-backend web_servers if client_is_stuck and http-request use-backend web_servers if !client_is_stuck directives ensure that whether a client is already "stuck" or is a new client, their request is eventually routed to the backend servers, provided they aren’t rate-limited. The http-request track-sc0 src table(rate_limit) ensures that every request (that isn’t immediately denied by rate limiting) increments the counter for its source IP.
The true power of stick tables lies in their in-memory, high-performance nature, making them ideal for these demanding tasks in a high-traffic environment. You can inspect the contents of your stick tables using HAProxy’s stats socket:
echo "show table session_stick" | socat stdio /var/run/haproxy.sock
echo "show table rate_limit" | socat stdio /var/run/haproxy.sock
This allows you to dynamically monitor which IPs are being tracked and how many requests they’ve made, which is invaluable for debugging and understanding traffic patterns.