A backend can stay available while one client or script sends enough traffic to crowd out other users. HAProxy can rate limit HTTP requests by client address, rejecting excess requests at the frontend before they consume backend capacity.
HAProxy keeps each address and its recent request rate in an in-memory stick table. A client above the threshold receives HTTP 429 Too Many Requests, while clients below it continue to the backend. The rate falls as requests age out of the measurement window, so the limit does not create a permanent ban.
Source-address limits fit a working HTTP frontend where HAProxy sees client addresses directly. Behind another proxy or CDN, the source address may identify that intermediary instead, grouping many users under one limit. A forwarded client address needs a trusted, sanitized header path before it can safely replace the connection's source address.
Steps to rate limit HTTP requests with HAProxy:
- Back up the active HAProxy configuration before adding the limit.
A low limit can reject legitimate requests, especially from users sharing a NAT address. The 20-request threshold below is for a staging frontend; a public site's limit depends on its traffic and backend capacity.
$ sudo cp -a /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.before-rate-limit
Debian and Ubuntu packages use /etc/haproxy/haproxy.cfg by default. If the running service loads another configuration file, that path belongs in the backup, edit, and validation commands.
- Open the active HAProxy configuration in your text editor.
$ sudoedit /etc/haproxy/haproxy.cfg
- Insert the rate-limit configuration below the site's existing HTTP frontend bind lines.
- /etc/haproxy/haproxy.cfg
stick-table type ipv6 size 100k expire 30s store http_req_rate(10s) http-request track-sc0 src acl exceeds_rate_limit sc_http_req_rate(0) gt 20 http-request deny deny_status 429 if exceeds_rate_limit
The lines belong in frontend fe_web if that is the site's frontend name. Existing listener, TLS, routing, and backend settings stay in place. The ipv6 key type accepts both IPv6 addresses and IPv4-mapped addresses. The table holds up to 102,400 entries, measures requests over 10 seconds, and expires an inactive entry after 30 seconds.
track-sc0 src updates counter 0 for the source address before the ACL reads it. gt 20 rejects requests when the measured rate exceeds 20 requests per 10 seconds. Rejected attempts also count toward the rate. These rules must precede any http-request allow rule that would skip the denial.
If this frontend already has a stick table or uses sticky counter 0, the existing design needs to be reconciled with these rules. A frontend cannot contain two stick-table definitions.
- Save the edited HAProxy configuration.
- Exit the editor to return to the shell.
sudoedit copies the saved changes into the active configuration when the editor exits.
- Validate the saved HAProxy configuration.
$ sudo haproxy -c -f /etc/haproxy/haproxy.cfg Configuration file is valid
Fatal parser errors must be resolved before a reload. A successful syntax check does not prove that the backend is reachable.
- Reload the HAProxy service to activate the limit.
$ sudo service haproxy reload
service is available with the Debian and Ubuntu HAProxy packages. The frontend-local table is held in memory and resets on reload unless peer synchronization is configured.
If the new policy blocks legitimate traffic, restoring haproxy.cfg.before-rate-limit over haproxy.cfg, validating it, and reloading the service returns to the previous policy.
- Check an ordinary request through the rate-limited frontend.
$ curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1/ 200The URL must reach HAProxy, not the backend directly. Here HAProxy listens locally on port 80; a different listener needs its own URL in each check. A read-only page that normally returns 200 makes the responses easy to compare.
- Test a burst of 25 requests from the same client address.
$ curl -sS -o /dev/null -w '%{http_code}\n' \ 'http://127.0.0.1/?rate-test=[1-25]' 200 200 ##### snipped ##### 200 429 429 429 429 429 429curl expands the quoted range into 25 URLs and prints one status per request. A burst completed within 10 seconds crosses the threshold and changes from 200 to 429. Other traffic from the same address affects when the first rejection appears.
If every response remains 200, the request may be reaching a different frontend, or the burst may be too slow to cross the configured threshold.
- Confirm that requests pass again after at least 30 seconds without traffic from the test address.
$ curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1/ 200A 200 response after the pause shows that the client can reach the backend again. Continued retries keep updating the rate and the entry's inactivity timer.