HAProxy’s stick tables, when combined with Access Control Lists (ACLs), allow you to implement sophisticated rate limiting that’s both granular and highly performant.
Let’s see this in action. Imagine you want to limit incoming HTTP requests to your web servers to a maximum of 10 requests per second per client IP address.
frontend http_in
bind *:80
mode http
acl rate_limited src_rate_limit 10
# If the client IP has exceeded the rate limit, deny the request
http-request deny if rate_limited
# Otherwise, let the request through to the backend
default_backend web_servers
backend web_servers
balance roundrobin
server web1 192.168.1.10:80 check
server web2 192.168.1.11:80 check
Here’s what’s happening:
acl rate_limited src_rate_limit 10: This is the core of our rate limiting.src_rate_limitis a special type of ACL that looks up the client’s source IP address in a stick table. If the count associated with that IP address for the current second exceeds10, therate_limitedACL evaluates to true.http-request deny if rate_limited: If therate_limitedACL is true, HAProxy will immediately deny the request with a429 Too Many Requestserror.
The magic behind src_rate_limit is HAProxy’s internal "stick table." When you use stick tables with rate limiting (or other tracking mechanisms like sc0 for server connection tracking), HAProxy maintains a dynamic table in memory. For src_rate_limit, the key is the client’s source IP address, and the value is a counter that increments for each request from that IP within a specific time window. By default, this window is one second.
The src_rate_limit ACL implicitly creates and uses a stick table. You don’t need to explicitly define it in the defaults or listen section for this basic usage. HAProxy manages its creation and population.
This setup is incredibly efficient because HAProxy processes requests in its event loop. When a request comes in, it checks the rate_limited ACL. If the IP is already over the limit, it’s dropped before any backend processing occurs. If it’s within the limit, the counter for that IP in the stick table is incremented, and the request is forwarded.
You can get much more granular. For instance, limiting requests based on a specific URL path:
frontend http_in
bind *:80
mode http
# ACL to identify requests to the /api path
acl is_api path /api*
# Rate limit API requests to 5 per second per IP
http-request track-sc0 src table api_rate_limit_table amount 1 if is_api
acl api_rate_limited sc0_rate_limit 5 if is_api
http-request deny if api_rate_limited
default_backend web_servers
backend web_servers
balance roundrobin
server web1 192.168.1.10:80 check
server web2 192.168.1.11:80 check
# Explicitly define the stick table for API requests
stick-table type ip size 1000000 expire 10s store ip,http_rate(1s)
In this more advanced example:
acl is_api path /api*: This ACL matches any request where the URL path starts with/api.http-request track-sc0 src table api_rate_limit_table amount 1 if is_api: This is crucial. It tells HAProxy to track the source IP (src) in a stick table namedapi_rate_limit_table. For every request matchingis_api, it increments the counter (amount 1) for that IP.sc0is just a placeholder for the first trackable field.acl api_rate_limited sc0_rate_limit 5 if is_api: This ACL checks the rate limit for the tracked source IP in theapi_rate_limit_table. If the rate exceeds 5 requests per second,api_rate_limitedbecomes true.stick-table type ip size 1000000 expire 10s store ip,http_rate(1s): This explicitly defines the stick table.type ip: The key for the table is an IP address.size 1000000: The table can hold up to a million entries.expire 10s: Entries that haven’t been accessed for 10 seconds will be removed.store ip,http_rate(1s): This specifies what data to store for each IP.ipstores the IP itself, andhttp_rate(1s)stores the rate of HTTP requests per second, sampled over a 1-second window.
The http_rate(1s) is the specific function that enables the rate limiting. It calculates the number of requests from a given source IP within the last second.
The expire parameter on the stick table is important for managing memory. If you have a very large number of unique IPs hitting your service, you don’t want old entries to persist forever. However, setting expire too low might mean legitimate users get temporarily blocked if their traffic pattern momentarily spikes and then drops off, as their entry might expire before they make their next request. A typical value might be between 10 seconds and 1 minute.
You can also store multiple data points in a single stick table, for example, to track both request rate and total number of requests for an IP. The store directive can list multiple items separated by commas, such as store ip,http_rate(1s),count.
The actual rate limiting logic can be placed in different parts of the HAProxy configuration. Placing it in the frontend is common for general rate limiting. If you need to rate limit based on backend server load or specific server health, you might place tracking and limiting logic within the backend section.
The most surprising thing about HAProxy’s stick tables is how they can be used for more than just rate limiting; they are a general-purpose in-memory key-value store accessible via ACLs. You can use them to track connection counts, session data, or even implement distributed session persistence for simple applications. The track-sc0 directive, for example, can be configured to store arbitrary counts or even custom data that you can then use in subsequent ACLs. This flexibility allows you to build complex application-aware logic directly within the load balancer.
The next step in rate limiting would be implementing distributed rate limiting across multiple HAProxy instances.