Introduction
HAProxy stick tables are one of the most powerful yet under-utilised features in the HAProxy ecosystem. They provide an in-memory key-value store that tracks connection metadata in real time — enabling you to implement rate limiting, connection throttling, abuse detection, and full DDoS mitigation without any external dependencies. Unlike tools that rely on external databases or third-party WAFs, stick tables operate entirely within HAProxy's event loop, adding near-zero latency to request processing.
This run book covers everything from basic stick table anatomy to advanced multi-layered defence patterns. Every configuration is production-tested and ready to deploy.
1. Stick Table Fundamentals
1.1 What Is a Stick Table?
A stick table is a fixed-size in-memory hash table attached to a frontend or backend. Each entry is keyed by a data type (IP address, integer, string, or binary) and can store multiple counters simultaneously. HAProxy updates these counters automatically as traffic flows through.
1.2 Core Syntax
backend st_src_tracking
stick-table type ip size 1m expire 10m nopurge peers mypeers store http_req_rate(10s),conn_cur,conn_rate(10s),gpc0,gpc0_rate(60s),bytes_out_rate(60s)
Let's break down each parameter:
- type ip — Key type. Options:
ip
,ipv6
,integer
,string len <n>
,binary len <n>
. - size 1m — Maximum number of entries (1 million). Memory usage depends on counters stored.
- expire 10m — Entries are evicted after 10 minutes of inactivity.
- nopurge — Prevents purging of entries when the table is full; new entries are rejected instead. Omit this for automatic LRU eviction.
- peers mypeers — Enables stick table replication (covered in section 6).
- store — Comma-separated list of counters to track.
1.3 Available Counters
The following counters can be stored in a stick table:
- conn_cnt — Total number of connections since entry creation.
- conn_cur — Current concurrent connections.
- conn_rate(<period>) — Connection rate over the specified period.
- http_req_cnt — Total HTTP requests.
- http_req_rate(<period>) — HTTP request rate over the specified period.
- http_err_cnt — Total HTTP errors (4xx/5xx from backend).
- http_err_rate(<period>) — HTTP error rate.
- http_fail_cnt — Total HTTP failures.
- http_fail_rate(<period>) — HTTP failure rate.
- bytes_in_cnt — Total bytes received from client.
- bytes_in_rate(<period>) — Bytes received rate.
- bytes_out_cnt — Total bytes sent to client.
- bytes_out_rate(<period>) — Bytes sent rate.
- gpc0 / gpc1 — General purpose counters (manually incremented).
- gpc0_rate(<period>) / gpc1_rate(<period>) — Rate of GPC increments.
- server_id — Last server ID used (for session persistence).
2. Tracking Connections with Sticky Counters
2.1 Understanding Sticky Counters (sc0, sc1, sc2)
HAProxy provides three sticky counter slots per connection:
sc0,
sc1, and
sc2. Each slot binds the current connection to one stick table entry. You can track three different dimensions simultaneously — for example, source IP in sc0, a URL path in sc1, and an API key header in sc2.
2.2 TCP-Level Tracking
Track the source IP as soon as the TCP connection is established:
frontend ft_web
bind *:80
bind *:443 ssl crt /etc/haproxy/certs/site.pem
# Track source IP at TCP layer using sc0
tcp-request connection track-sc0 src table st_src_tracking
# Reject if too many concurrent connections from one IP
tcp-request connection reject if { src_conn_cur(st_src_tracking) gt 50 }
default_backend bk_web
backend st_src_tracking
stick-table type ip size 500k expire 5m store conn_cur,conn_rate(10s),http_req_rate(10s),http_err_rate(10s)
2.3 HTTP-Level Tracking
Track at the HTTP layer for finer-grained control:
frontend ft_web
bind *:443 ssl crt /etc/haproxy/certs/site.pem
tcp-request connection track-sc0 src table st_src_tracking
# Track at HTTP layer — needed for http_req_rate counters
http-request track-sc1 src table st_http_tracking
# Rate limit based on HTTP request rate
http-request deny deny_status 429 if { sc_http_req_rate(1) gt 100 }
default_backend bk_web
backend st_src_tracking
stick-table type ip size 500k expire 5m store conn_cur,conn_rate(10s)
backend st_http_tracking
stick-table type ip size 500k expire 5m store http_req_rate(10s),http_err_rate(10s)
Key insight:
conn_rateand
conn_curare updated at the TCP layer.
http_req_rate,
http_err_rate, and related HTTP counters require the request to be parsed, so tracking must happen via
http-request track-scor at minimum after the TCP content inspection phase.
3. Rate Limiting Patterns
3.1 Basic Request Rate Limiting
Deny any source IP making more than 60 requests in 10 seconds:
frontend ft_api
bind *:443 ssl crt /etc/haproxy/certs/api.pem
# Whitelist internal monitoring
acl is_monitor src 10.0.0.0/8 172.16.0.0/12
http-request track-sc0 src table st_api_rate
http-request deny deny_status 429 if !is_monitor { sc_http_req_rate(0) gt 60 }
# Return a Retry-After header on 429
http-response set-header Retry-After 10 if { status 429 }
default_backend bk_api
backend st_api_rate
stick-table type ip size 200k expire 2m store http_req_rate(10s)
3.2 Tiered Rate Limiting (Soft + Hard)
Apply a soft limit (delay via tarpit) before a hard deny:
frontend ft_api
bind *:443 ssl crt /etc/haproxy/certs/api.pem
http-request track-sc0 src table st_tiered_rate
# Hard limit: 200 req/10s → deny
acl is_hard_abuse sc_http_req_rate(0) gt 200
http-request deny deny_status 429 if is_hard_abuse
# Soft limit: 100 req/10s → tarpit for 5s (holds connection open)
acl is_soft_abuse sc_http_req_rate(0) gt 100
http-request tarpit deny_status 429 if is_soft_abuse
timeout tarpit 5s
default_backend bk_api
backend st_tiered_rate
stick-table type ip size 200k expire 2m store http_req_rate(10s)
Note: Tarpit holds the connection open silently, consuming attacker resources. The response is only sent after the
timeout tarpitperiod.
3.3 Per-URL Rate Limiting
Rate-limit expensive endpoints like login or search independently:
frontend ft_web
bind *:443 ssl crt /etc/haproxy/certs/site.pem
# General tracking
http-request track-sc0 src table st_global_rate
# Track login endpoint separately — combine IP + path as key
acl is_login path_beg /api/login
http-request track-sc1 src table st_login_rate if is_login
# Global: 200 req/10s
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 200 }
# Login: 10 req/60s
http-request deny deny_status 429 if is_login { sc_http_req_rate(1) gt 10 }
default_backend bk_web
backend st_global_rate
stick-table type ip size 500k expire 2m store http_req_rate(10s)
backend st_login_rate
stick-table type ip size 200k expire 5m store http_req_rate(60s)
3.4 Rate Limiting by API Key
Track by a custom header instead of IP:
frontend ft_api
bind *:443 ssl crt /etc/haproxy/certs/api.pem
http-request track-sc0 req.hdr(X-API-Key) table st_apikey_rate if { req.hdr(X-API-Key) -m found }
# Fallback: track by IP if no API key
http-request track-sc1 src table st_ip_rate unless { req.hdr(X-API-Key) -m found }
# API key limit: 1000 req/60s
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 1000 }
# IP fallback limit: 30 req/60s
http-request deny deny_status 429 if { sc_http_req_rate(1) gt 30 }
default_backend bk_api
backend st_apikey_rate
stick-table type string len 64 size 100k expire 5m store http_req_rate(60s)
backend st_ip_rate
stick-table type ip size 200k expire 5m store http_req_rate(60s)
4. Connection Tracking and Abuse Detection
4.1 Slowloris / Slow Connection Detection
Detect and reject slow clients that open many connections but send data slowly:
frontend ft_web
bind *:443 ssl crt /etc/haproxy/certs/site.pem
tcp-request connection track-sc0 src table st_slowloris
# More than 20 concurrent connections from a single IP
tcp-request connection reject if { src_conn_cur(st_slowloris) gt 20 }
# More than 40 new connections in 10 seconds
tcp-request connection reject if { src_conn_rate(st_slowloris) gt 40 }
# Aggressive timeouts to kill slow clients
timeout client 15s
timeout http-request 5s
timeout http-keep-alive 5s
default_backend bk_web
backend st_slowloris
stick-table type ip size 500k expire 2m store conn_cur,conn_rate(10s)
4.2 Error Rate Monitoring (Credential Stuffing Detection)
Detect IPs generating a high rate of HTTP errors (failed logins return 401/403):
frontend ft_web
bind *:443 ssl crt /etc/haproxy/certs/site.pem
http-request track-sc0 src table st_error_tracking
# Block if more than 30 errors in 60 seconds
acl is_bruteforce sc_http_err_rate(0) gt 30
http-request deny deny_status 403 if is_bruteforce
default_backend bk_web
backend st_error_tracking
stick-table type ip size 200k expire 10m store http_err_rate(60s),http_err_cnt
4.3 Using General Purpose Counters (GPC) for Flagging
GPC counters let you "flag" an IP for custom conditions. For example, flag an IP as abusive and block it for a period:
frontend ft_web
bind *:443 ssl crt /etc/haproxy/certs/site.pem
http-request track-sc0 src table st_gpc_flag
# If already flagged, deny immediately
http-request deny deny_status 403 if { src_get_gpc0(st_gpc_flag) gt 0 }
# Flag if request rate exceeds 150/10s
acl is_abusive sc_http_req_rate(0) gt 150
http-request sc-set-gpt0(0) 1 if is_abusive
http-request sc-inc-gpc0(0) if is_abusive
default_backend bk_web
backend st_gpc_flag
stick-table type ip size 200k expire 30m store http_req_rate(10s),gpc0,gpc0_rate(60s)
Once flagged, the IP is denied for the full 30-minute expiry window, even if the request rate drops. This is useful for blacklisting repeat offenders longer than the initial burst.
5. DDoS Mitigation Strategy
5.1 Multi-Layer Defence Configuration
A comprehensive DDoS mitigation setup combines TCP-level and HTTP-level protections:
# ===========================================
# Global settings
# ===========================================
global
log /dev/log local0 info
log /dev/log local1 notice
maxconn 100000
tune.ssl.default-dh-param 2048
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
# ===========================================
# Defaults
# ===========================================
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5s
timeout client 15s
timeout server 15s
timeout http-request 5s
timeout http-keep-alive 5s
timeout queue 30s
timeout tarpit 5s
# ===========================================
# Stick tables (dedicated backends)
# ===========================================
backend st_tcp_conntrack
stick-table type ip size 1m expire 5m store conn_cur,conn_rate(5s)
backend st_http_ratelimit
stick-table type ip size 1m expire 5m store http_req_rate(10s),http_err_rate(60s)
backend st_http_abuse
stick-table type ip size 500k expire 60m store gpc0,http_req_cnt
# ===========================================
# Frontend: DDoS-hardened
# ===========================================
frontend ft_https
bind *:443 ssl crt /etc/haproxy/certs/site.pem alpn h2,http/1.1
bind *:80
redirect scheme https code 301 unless { ssl_fc }
# ---- Whitelist trusted sources ----
acl is_trusted src 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
acl is_healthcheck path /health
# ---- Layer 4: Connection tracking ----
tcp-request connection track-sc0 src table st_tcp_conntrack
tcp-request connection reject if !is_trusted { src_conn_cur(st_tcp_conntrack) gt 80 }
tcp-request connection reject if !is_trusted { src_conn_rate(st_tcp_conntrack) gt 60 }
# ---- Layer 7: HTTP rate tracking ----
http-request track-sc1 src table st_http_ratelimit
http-request track-sc2 src table st_http_abuse
# ---- Check if already flagged as abusive ----
acl is_flagged src_get_gpc0(st_http_abuse) gt 0
http-request deny deny_status 403 if is_flagged !is_trusted !is_healthcheck
# ---- Hard deny: >300 req/10s ----
acl is_flood sc_http_req_rate(1) gt 300
http-request deny deny_status 429 if is_flood !is_trusted
# ---- Flag abusers: >500 req/10s → flagged for 60 minutes ----
acl is_severe_flood sc_http_req_rate(1) gt 500
http-request sc-inc-gpc0(2) if is_severe_flood !is_trusted
# ---- Tarpit: 150-300 req/10s ----
acl is_suspicious sc_http_req_rate(1) gt 150
http-request tarpit deny_status 429 if is_suspicious !is_flood !is_trusted
# ---- Error rate abuse: >50 errors/60s ----
acl is_error_abuse sc_http_err_rate(1) gt 50
http-request deny deny_status 403 if is_error_abuse !is_trusted
# ---- Custom 429 response header ----
http-response set-header Retry-After 30 if { status 429 }
# ---- Logging tagged connections ----
http-request set-header X-RateLimit-Remaining %[sc_http_req_rate(1)]
http-request add-header X-Flagged true if is_flagged
default_backend bk_web
# ===========================================
# Backend
# ===========================================
backend bk_web
balance roundrobin
option httpchk GET /health HTTP/1.1\r\nHost:\ localhost
http-check expect status 200
server web1 10.0.1.10:8080 check inter 3s fall 3 rise 2
server web2 10.0.1.11:8080 check inter 3s fall 3 rise 2
server web3 10.0.1.12:8080 check inter 3s fall 3 rise 2
5.2 Explanation of Defence Layers
- Layer 4 — Connection Limiting: Drops TCP connections from IPs with >80 concurrent or >60 new connections in 5 seconds. Stops SYN floods before HTTP parsing.
- Layer 7 — Soft Tarpit: IPs sending 150–300 req/10s are tarpitted for 5 seconds per request, wasting attacker resources.
- Layer 7 — Hard Deny: IPs exceeding 300 req/10s receive an immediate 429.
- Layer 7 — Flagging: IPs exceeding 500 req/10s are flagged via GPC0, blocking them for 60 minutes even if the attack stops.
- Error Rate: IPs generating >50 backend errors/minute are blocked (catches credential stuffing and fuzzing).
6. Stick Table Replication with Peers
For high-availability setups, stick tables must be synchronised across HAProxy instances:
peers mypeers
peer haproxy1 10.0.1.1:1024
peer haproxy2 10.0.1.2:1024
backend st_src_tracking
stick-table type ip size 1m expire 5m peers mypeers store conn_cur,conn_rate(10s),http_req_rate(10s),gpc0
Key notes on peer replication:
- Each HAProxy instance must have a
peer
entry matching its own hostname (the hostname must matchhostname
output or be set explicitly). - Port 1024 (or any chosen port) must be open between peers — ensure firewall rules allow it.
- Replication is asynchronous; there is a brief window where one node may not have the latest counters.
- Use
bind-process
if using multi-process mode to ensure the peers section runs on the correct process.
# Verify peers are connected via the stats socket
echo "show peers" | socat stdio /run/haproxy/admin.sock
7. Monitoring and Managing Stick Tables
7.1 Runtime API Commands
HAProxy's runtime API (via the stats socket) provides powerful stick table management:
# Show all stick tables and their sizes
echo "show table" | socat stdio /run/haproxy/admin.sock
# Show contents of a specific table
echo "show table st_http_ratelimit" | socat stdio /run/haproxy/admin.sock
# Show a specific entry
echo "show table st_http_ratelimit data.http_req_rate gt 50" | socat stdio /run/haproxy/admin.sock
# Manually set gpc0 to flag an IP
echo "set table st_http_abuse key 203.0.113.50 data.gpc0 1" | socat stdio /run/haproxy/admin.sock
# Clear a specific entry (unblock an IP)
echo "clear table st_http_abuse key 203.0.113.50" | socat stdio /run/haproxy/admin.sock
# Clear all entries in a table
echo "clear table st_http_ratelimit" | socat stdio /run/haproxy/admin.sock
7.2 Exporting Stick Table Data for Monitoring
Create a script that exports top offenders to your monitoring system:
#!/bin/bash
# /usr/local/bin/haproxy-top-offenders.sh
# Export top IPs by request rate to a file for Prometheus/Grafana
SOCKET="/run/haproxy/admin.sock"
TABLE="st_http_ratelimit"
THRESHOLD=50
OUTPUT="/var/log/haproxy/top_offenders.log"
echo "show table ${TABLE} data.http_req_rate gt ${THRESHOLD}" \
| socat stdio "${SOCKET}" \
| awk 'NR>1 {print strftime("%Y-%m-%d %H:%M:%S"), $0}' \
>> "${OUTPUT}"
# Optional: send to syslog
logger -t haproxy-ratelimit -p local0.warning "$(wc -l < ${OUTPUT}) IPs above threshold ${THRESHOLD}"
7.3 Stats Page Integration
Enable the stats page to visually inspect table sizes:
frontend ft_stats
bind 127.0.0.1:8404
mode http
stats enable
stats uri /stats
stats refresh 5s
stats show-legends
stats show-node
stats admin if TRUE
8. Memory Sizing Guide
Stick table memory usage depends on the key type and stored counters. Use this formula to estimate:
Memory ≈ entries × (key_size + 40 bytes base + Σ counter_sizes)
Approximate counter sizes:
- Each simple counter (conn_cnt, http_req_cnt, gpc0, etc.): ~8 bytes
- Each rate counter (conn_rate, http_req_rate, etc.): ~16 bytes
- IP key: 4 bytes (IPv4) or 16 bytes (IPv6)
- String key: len + 4 bytes
Example calculation for 1M entries with IPv4 key storing
conn_cur,conn_rate(10s),http_req_rate(10s),gpc0:
Per entry = 4 (key) + 40 (base) + 8 (conn_cur) + 16 (conn_rate) + 16 (http_req_rate) + 8 (gpc0) = 92 bytes
Total = 1,000,000 × 92 = ~88 MB
Ensure your HAProxy instance has sufficient RAM. Set
global
maxconnand OS-level limits accordingly.
9. Testing Your Configuration
9.1 Validate Configuration Syntax
haproxy -c -f /etc/haproxy/haproxy.cfg
9.2 Simulate Load with curl
# Quick burst test — send 200 requests rapidly
for i in $(seq 1 200); do
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/api/test
done | sort | uniq -c | sort -rn
9.3 Load Test with hey (recommended)
# Install hey
go install github.com/rakyll/hey@latest
# Send 500 requests, 50 concurrent
hey -n 500 -c 50 https://example.com/api/test
# Check stick table after test
echo "show table st_http_ratelimit" | socat stdio /run/haproxy/admin.sock
9.4 Test from Multiple IPs
# Use different source IPs via network namespaces (Linux)
for ip in 192.168.100.{10..15}; do
ip addr add ${ip}/24 dev eth0 2>/dev/null
curl --interface ${ip} -s -o /dev/null -w "${ip}: %{http_code}\n" https://example.com/
ip addr del ${ip}/24 dev eth0 2>/dev/null
done
10. Production Best Practices
- Always whitelist monitoring and internal IPs — Use ACLs to bypass rate limiting for health checks, Prometheus scrapers, and internal services.
- Start with soft limits — Deploy with logging only (no deny) first. Analyse the data before enforcing blocks.
- Use separate tables for separate concerns — Don't cram all counters into one table. Use dedicated tables for TCP tracking, HTTP rate limiting, and abuse flagging.
- Set appropriate expire times — Short expiry (2-5 min) for rate tables, longer expiry (30-60 min) for abuse flag tables.
- Monitor table utilisation — Alert when a table reaches 80% capacity. An eviction storm can cause legitimate entries to be lost.
- Log denied requests — Add
http-request capture
and custom log-format to track denials for forensics. - Test replication — In HA setups, regularly verify peers are synced and tables are consistent.
- Document your thresholds — Keep a run book of each threshold, why it was chosen, and when it was last reviewed.
# Custom log format that includes stick table counters
log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs {%[sc_http_req_rate(1)]|%[src_conn_cur]} %{+Q}r"
11. Troubleshooting Common Issues
Issue: Stick table counters always show 0
Cause: The
track-scdirective is placed after the
denyrule, so the counter is never incremented.
Fix: Always place
track-scdirectives before any deny/tarpit rules.
Issue: Rate limiting doesn't work on HTTP/2 connections
Cause: HTTP/2 multiplexes many requests over a single TCP connection.
conn_rateonly counts new TCP connections.
Fix: Use
http_req_rateinstead of
conn_ratefor HTTP/2 environments.
Issue: Legitimate users behind NAT are being blocked
Cause: Many users share a single source IP. IP-based tracking can cause collateral blocking.
Fix: Use a combination of IP + header (e.g.,
X-Forwarded-For, session cookie, or API key) as the stick table key, or increase thresholds for known NAT ranges.
# Track by X-Forwarded-For if present, otherwise by src
acl has_xff req.hdr(X-Forwarded-For) -m found
http-request track-sc0 req.hdr_ip(X-Forwarded-For) table st_rate if has_xff
http-request track-sc0 src table st_rate unless has_xff
Issue: Table fills up and entries are not evicted
Cause:
nopurgeis set and the table has reached
size.
Fix: Remove
nopurgeto allow LRU eviction, or increase the table size.
