HAProxy doesn’t just count requests; it can dynamically punish clients that go too far, and it does it by silently tracking them in memory.

Let’s say you’re seeing a surge of traffic from a single IP address that’s hitting your API endpoints repeatedly, potentially overwhelming your backend or just being a nuisance. You want to slow them down, maybe even block them for a while. HAProxy’s stick tables are your best friend here.

Here’s how it looks in practice. First, you need to configure a stick table in your haproxy.cfg. This table will store information about clients, keyed by their IP address.

frontend http_in
    bind *:80
    mode http
    acl abusive_path path_beg /api/
    stick-table type ip size 100000 expire 10s store gpc0,conn_rate(60s)
    http-request deny if { src -m found } !abusive_path
    http-request track-sc0 src if abusive_path
    http-request deny if { sc0_conn_rate gt 100 }
    http-request allow

Let’s break this down.

  • stick-table type ip size 100000 expire 10s store gpc0,conn_rate(60s): This is the core.

    • type ip: We’re using client IP addresses as the keys for our table.
    • size 100000: This is the maximum number of entries (IP addresses) the table can hold. A good starting point, adjust based on your expected unique client IPs.
    • expire 10s: How long an entry stays in the table if it’s not updated. This is quite short; we’ll store more granular rates.
    • store gpc0,conn_rate(60s): This is crucial. gpc0 is a generic counter we can increment. conn_rate(60s) tracks the number of connections from an IP in the last 60 seconds. HAProxy will automatically populate this.
  • acl abusive_path path_beg /api/: This defines an ACL that matches requests to our /api/ path. We only want to rate-limit API traffic.

  • http-request deny if { src -m found } !abusive_path: This is a simple protection. If an IP is already in the stick table (meaning it’s been tracked for something), and it’s NOT hitting the API, deny it. This prevents a client we’ve flagged for API abuse from then hitting other parts of your site.

  • http-request track-sc0 src if abusive_path: For any request matching abusive_path, we track the source IP (src) and increment a counter (sc0). This counter is linked to the stick table entry for that IP.

  • http-request deny if { sc0_conn_rate gt 100 }: This is the actual rate-limiting logic. If the connection rate for the source IP (sc0_conn_rate) in the last 60 seconds exceeds 100, deny the request. This is a hard limit.

  • http-request allow: If none of the above deny rules matched, allow the request.

The conn_rate(60s) stored in the stick table automatically updates. When a request comes in for an IP that’s already in the table, HAProxy checks if its conn_rate(60s) has exceeded 100. If it has, the http-request deny if { sc0_conn_rate gt 100 } rule fires. If it hasn’t, the track-sc0 src adds to the sc0 counter associated with that IP, and the conn_rate(60s) metric is updated.

This setup is powerful because it’s stateful. HAProxy remembers the clients. The expire 10s on the table itself means an IP will be forgotten relatively quickly if it stops sending traffic. However, the conn_rate(60s) metric persists for 60 seconds regardless of the table expiration. So, even if the IP entry is removed from the table after 10 seconds of inactivity, its conn_rate(60s) value is still relevant for the next 60 seconds. If a client is aggressive, it will continuously update its entry and its conn_rate(60s) will stay high.

You can also use gpc0 for a more manual rate-limiting approach. For example, to block an IP after 1000 requests within a short window:

frontend http_in
    bind *:80
    mode http
    acl abusive_path path_beg /api/
    stick-table type ip size 100000 expire 1m store gpc0
    http-request track-sc0 src if abusive_path
    http-request deny if { sc0_gpc0 gt 1000 }
    http-request allow

Here, http-request track-sc0 src if abusive_path increments sc0_gpc0 for each API request from a given source IP. The stick-table expire 1m keeps the IP in the table for a minute. If sc0_gpc0 exceeds 1000, the request is denied. The expire 1m on the stick table ensures that an IP that stops sending requests will eventually be removed from memory, freeing up space.

The store directive is key here. You can store multiple metrics per IP. For example, store gpc0,conn_rate(60s),bytes_in(10s) allows you to track request counts, connection rates, and incoming bytes over different time windows for the same IP.

The expire time on the stick table is important. If set too low, an IP might be removed from the table before its rate-limited state is fully enforced. If set too high, memory usage can grow. A common pattern is to use a relatively short expire for the table (e.g., 1m or 5m) and rely on the rate() or conn_rate() functions which have their own internal time windows.

What many people miss is that HAProxy’s stick tables are not persistent. They live entirely in RAM. If HAProxy restarts, all stick table data is lost. For persistent rate limiting across restarts, you’d need an external solution or more complex HAProxy configurations with persistent storage (which is significantly more involved).

The next thing you’ll run into is needing to dynamically adjust these limits based on real-time traffic patterns, which often leads to exploring HAProxy’s Lua scripting capabilities.

Want structured learning?

Take the full Haproxy course →