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_limit is 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 exceeds 10, the rate_limited ACL evaluates to true.
  • http-request deny if rate_limited: If the rate_limited ACL is true, HAProxy will immediately deny the request with a 429 Too Many Requests error.

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 named api_rate_limit_table. For every request matching is_api, it increments the counter (amount 1) for that IP. sc0 is 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 the api_rate_limit_table. If the rate exceeds 5 requests per second, api_rate_limited becomes 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. ip stores the IP itself, and http_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.

Want structured learning?

Take the full Rate-limiting course →