Rate LimitingHAProxy14 min read

HAProxy Rate Limiting: Complete Configuration Guide with Stick Tables (2025)

HAProxy's stick tables are one of its most powerful and underused features. This guide covers everything: connection rate limiting, HTTP request rate limiting, per-user API limits, automatic IP banning, and how to tune stick tables for production.

PS
PentaSynth Team
January 20, 2025 · Updated March 2025

Understanding Stick Tables

A stick table is an in-memory key-value store that HAProxy maintains. You define what key to track (e.g., source IP, session cookie, URL parameter) and what counters to maintain (e.g., connection count, request rate, error rate). HAProxy updates these counters in real time and you can use them in ACL rules to make routing decisions.

The general syntax:

backend my_stick_table
    stick-table type <KEY_TYPE> size <MAX_ENTRIES> expire <TTL> \
        store <COUNTER1>,<COUNTER2>,...

Stick Table Key Types

  • ip — IPv4 address (4 bytes)
  • ipv6 — IPv6 address (16 bytes)
  • integer — An integer value
  • string — A string value (up to 100 bytes)
  • binary — Binary data

Available Counters

  • conn_cur — Current active connections
  • conn_rate(<period>) — Connection rate over period
  • http_req_rate(<period>) — HTTP request rate over period
  • http_err_rate(<period>) — HTTP error rate over period
  • http_fail_rate(<period>) — Failed connection rate
  • bytes_in_rate(<period>) — Inbound bytes per period
  • gpc0 — General purpose counter (you control this)
  • gpc0_rate(<period>) — Rate of gpc0 increments

Level 1: Connection Rate Limiting (TCP Layer)

TCP-layer rate limiting happens before HAProxy parses any HTTP. This is your first line of defense against connection floods:

# Create a separate backend just for tracking — it has no real servers
backend conn_tracker
    stick-table type ip size 200k expire 30s \
        store conn_cur,conn_rate(3s)

frontend web_frontend
    bind *:443 ssl crt /etc/ssl/certs/site.pem

    # Track each source IP in the conn_tracker table
    tcp-request connection track-sc0 src table conn_tracker

    # Reject if more than 100 concurrent connections from one IP
    tcp-request connection reject if { sc_conn_cur(0) gt 100 }

    # Reject if connecting at more than 200 connections per 3 seconds
    tcp-request connection reject if { sc_conn_rate(0) gt 200 }

    default_backend web_servers

The sc_ prefix stands for “sticky counter.” The number in parentheses refers to the tracking slot (0, 1, or 2 — you can track up to 3 tables per connection).

Level 2: HTTP Request Rate Limiting

After the TCP connection is established, you can rate limit at the HTTP layer:

backend http_rate_tracker
    stick-table type ip size 200k expire 60s \
        store http_req_rate(10s),http_err_rate(30s)

frontend web_frontend
    bind *:443 ssl crt /etc/ssl/certs/site.pem
    mode http

    # Track HTTP request rate per IP
    http-request track-sc0 src table http_rate_tracker

    # Return 429 Too Many Requests if over 100 requests in 10 seconds
    http-request deny deny_status 429 \
        if { sc_http_req_rate(0) gt 100 }

    # Custom response for rate limited requests
    http-request return status 429 content-type "application/json" \
        string '{"error":"rate_limit_exceeded","retry_after":10}' \
        if { sc_http_req_rate(0) gt 100 }

    default_backend web_servers

Level 3: Different Rate Limits for Different Endpoints

Not all endpoints need the same limit. Your login endpoint should have a tighter limit than your static assets:

backend api_rate_tracker
    stick-table type ip size 100k expire 60s store http_req_rate(60s)

backend auth_rate_tracker
    # Much tighter table for auth endpoints
    stick-table type ip size 100k expire 300s store http_req_rate(60s)

frontend web_frontend
    bind *:443 ssl crt /etc/ssl/certs/site.pem
    mode http

    # Define path ACLs
    acl is_auth_endpoint path_beg /auth /login /api/token /password-reset
    acl is_api_endpoint  path_beg /api/

    # Apply different tracking and limits per path type
    http-request track-sc0 src table auth_rate_tracker if is_auth_endpoint
    http-request track-sc1 src table api_rate_tracker  if is_api_endpoint

    # Auth endpoints: max 10 requests per minute
    http-request deny deny_status 429 \
        if is_auth_endpoint { sc_http_req_rate(0) gt 10 }

    # API endpoints: max 300 requests per minute
    http-request deny deny_status 429 \
        if is_api_endpoint { sc_http_req_rate(1) gt 300 }

    default_backend web_servers

Level 4: Automatic IP Banning

For persistent attackers, you want to ban their IP for hours, not just rate limit per request. Use a separate ban table with a long TTL:

backend ban_list
    # Store banned IPs for 24 hours
    stick-table type ip size 100k expire 24h store gpc0

backend error_tracker
    stick-table type ip size 100k expire 5m store http_err_rate(3m)

frontend web_frontend
    bind *:443 ssl crt /etc/ssl/certs/site.pem
    mode http

    # ---- Check ban list first (before any processing) ----
    # If IP is in ban list (gpc0 > 0), reject immediately
    http-request deny status 403 \
        if { src,table_gpc0(ban_list) gt 0 }

    # ---- Track error rate ----
    http-request track-sc0 src table error_tracker

    # ---- Auto-ban: if >50 errors in 3 minutes, add to ban list ----
    http-request track-sc1 src table ban_list \
        if { sc_http_err_rate(0) gt 50 }
    http-request sc-inc-gpc0(1) \
        if { sc_http_err_rate(0) gt 50 }

    default_backend web_servers

Level 5: Per-User Rate Limiting (API Keys)

For APIs that use bearer tokens or API keys, you can rate limit by token rather than by IP (which is more accurate for legitimate users behind NAT or CDNs):

backend api_key_tracker
    stick-table type string len 64 size 100k expire 60s \
        store http_req_rate(60s)

frontend api_frontend
    bind *:443 ssl crt /etc/ssl/certs/site.pem
    mode http

    # Extract API key from header
    acl has_api_key req.hdr(X-API-Key) -m found

    # Track by API key string (not IP)
    http-request track-sc0 req.hdr(X-API-Key) table api_key_tracker \
        if has_api_key

    # 1000 requests per minute per API key
    http-request deny deny_status 429 \
        if has_api_key { sc_http_req_rate(0) gt 1000 }

    default_backend api_servers

Stick Table Sizing: How to Calculate

Sizing your stick table wrong wastes memory or loses tracking data. Here's how to calculate the right size:

  • type ip, store conn_cur,conn_rate(3s) — Each entry uses ~100 bytes. For 100k unique IPs: 100k × 100 bytes = 10 MB.
  • size — Number of entries. Set to 2-3x your expected unique visitor count per expire window. When full, HAProxy silently drops the oldest entries.
  • expire — How long inactive entries are kept. Set to your rate window plus a buffer. A 10s rate window with 30s expiry works well.
# For a site with ~50K unique IPs per minute:
# type ip = 4 bytes key
# store conn_rate(3s) = ~8 bytes
# overhead = ~20 bytes
# Total per entry: ~32 bytes
# 50K entries × 32 bytes = 1.6 MB — very manageable

backend sized_tracker
    stick-table type ip size 50k expire 30s store conn_rate(3s)

Testing Your Rate Limits

# Test with wrk - send 200 requests in 10 seconds
wrk -t1 -c1 -d10s --latency https://your-site.com/api/test

# View current stick table state via runtime API
echo "show table conn_tracker" | nc -U /var/run/haproxy/admin.sock

# See entries in the ban list
echo "show table ban_list" | nc -U /var/run/haproxy/admin.sock

# Manually unban an IP
echo "clear table ban_list key 1.2.3.4" | nc -U /var/run/haproxy/admin.sock

Complete Production Rate Limiting Config

# Stick tables (pure tracking backends, no real servers)
backend conn_tracker
    stick-table type ip size 200k expire 30s store conn_cur,conn_rate(3s)

backend http_tracker
    stick-table type ip size 200k expire 60s \
        store http_req_rate(10s),http_err_rate(3m)

backend auth_tracker
    stick-table type ip size 50k expire 300s store http_req_rate(60s)

backend ban_list
    stick-table type ip size 100k expire 24h store gpc0

# Main frontend
frontend web_frontend
    bind *:443 ssl crt /etc/ssl/certs/site.pem
    mode http

    # Layer 1: TCP connection rate (early rejection)
    tcp-request connection track-sc0 src table conn_tracker
    tcp-request connection reject if { sc_conn_rate(0) gt 200 }
    tcp-request connection reject if { sc_conn_cur(0)  gt 100 }

    # Layer 2: Check ban list
    http-request deny status 403 if { src,table_gpc0(ban_list) gt 0 }

    # Layer 3: HTTP request rate tracking
    http-request track-sc1 src table http_tracker

    # Layer 4: Auto-ban persistent error sources
    http-request track-sc2 src table ban_list
    http-request sc-inc-gpc0(2) if { sc_http_err_rate(1) gt 50 }

    # Layer 5: Endpoint-specific rate limits
    acl is_auth path_beg /login /auth /api/token
    http-request track-sc0 src table auth_tracker if is_auth
    http-request deny status 429 if is_auth { sc_http_req_rate(0) gt 10 }

    # Layer 6: Global rate limit
    http-request deny deny_status 429 if { sc_http_req_rate(1) gt 200 }

    default_backend web_servers

Getting attacked? Rate limiting not working as expected?

Our team has implemented HAProxy rate limiting for APIs handling millions of requests per day. We diagnose and fix production issues fast.

Get Help Now

Need Production HAProxy Rate Limiting?

PentaSynth implements multi-layer rate limiting, DDoS protection, and auto-banning for production APIs — as part of our DevOps & Cloud service.

See Our Services