skip to content

You need an HAProxy frontend to answer 429 to any client IP that exceeds 100 requests in 10 seconds. Walk through the `stick-table`, `http-request track-sc0` and `http-request deny` lines required, and explain what order they must appear in and why.

level: middleimportance: must knowfreq 65%

answer

  1. track first, then test the counter
  2. the counter slot is sc0
  3. deny defaults to 403
  4. rate is a moving average
  5. src is whoever opened the connection

basics

~20 s

Declare a table storing http_req_rate(10s), track each request against it with http-request track-sc0 src, then deny with http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }. The track rule must come first, because the deny rule reads the counter that tracking populates.

solid answer

~50 s

Three lines in the frontend do it. First `stick-table type ip size 100k expire 10m store http_req_rate(10s)` creates a per-source-address table holding a request rate averaged over ten seconds. Then `http-request track-sc0 src` attaches sticky counter zero to that client's entry, creating it if needed and incrementing the request counter for every request. Finally `http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }` reads the tracked counter and rejects once it exceeds the threshold. Order matters because HAProxy evaluates `http-request` rules top to bottom: put the deny first and `sc_http_req_rate(0)` reads an untracked counter, which is always zero, so nothing is ever blocked. Two other details bite in practice. `deny` returns 403 unless you set `deny_status`, and 429 is what a rate-limited client should see. And `src` is the address of whoever opened the TCP connection — behind a CDN or another proxy that is one upstream address, so every user shares a single entry and the limit fires for everybody at once.

code

ini · 6 lines
ini
frontend fe_public
    bind :80
    stick-table type ip size 100k expire 10m store http_req_rate(10s)
    http-request track-sc0 src
    http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }
    default_backend app

go deeper

for a junior

Be able to name the three lines — declare the table, track the client, deny above the threshold — and say that the deny needs deny_status 429 to return anything other than 403.

for a middle

Explain rule evaluation order and why a deny above the track rule silently never fires, and that in HTTP mode every request on a keep-alive connection increments the counter.

for a senior

Show the key-selection judgment: src behind a CDN throttles the whole site from one entry, a client-settable forwarded header can be rotated to escape the limit, and NAT makes per-IP limits unfair. Describe rolling the rule out in observation mode first.

for a principal

Decide where throttling belongs at all — edge proxy, gateway, or per-tenant inside the service — and set the platform standard for the key, the response code and how thresholds are reviewed as traffic grows.

## The configuration ``` frontend fe_public bind :80 stick-table type ip size 100k expire 10m store http_req_rate(10s) http-request track-sc0 src http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 } default_backend app ``` The pieces: - **The table** holds one entry per source address with a request rate averaged over ten seconds. Because it is declared in this frontend, rules here use it implicitly; a rule in another section would need `table fe_public`. - **`track-sc0 src`** binds *sticky counter zero* for this connection to the entry keyed by the source address. Tracking is what creates the entry and what makes each request increment `http_req_cnt` and feed `http_req_rate`. HAProxy provides several sticky counter slots, `sc0`, `sc1`, `sc2` and beyond, so you can track more than one key at once — for example the address in `sc0` and an API key header in `sc1`. - **`sc_http_req_rate(0)`** reads the request rate from whatever counter zero is tracking. The `(0)` is the counter slot, not a period. ## Why the order is not negotiable `http-request` rules run in file order for each request. If the deny sits above the track, the fetch resolves against a counter that is not bound to anything and returns zero, so the condition is never true and the rule silently does nothing. This is the single most common way a rate limit is written and never fires. The symptom is indistinguishable from a threshold that is too high, which is why reading rule order is the first diagnostic step. A related subtlety: `track-sc0` is ignored if counter zero is already tracking something on that connection. A second `track-sc0` further down — perhaps one keying on a header — does not replace the first. Use a different slot instead. ## What the counter actually measures `http_req_rate` is a moving average over the declared period, computed from the current and previous interval rather than a precise count in a fixed bucket. That means a threshold behaves approximately: a very short burst can pass under a limit that a sustained equivalent load would trip. If you need a ceiling on concurrency rather than rate, `conn_cur` is the data type to store, and if you want to count something the built-ins do not cover, increment a general-purpose counter with `http-request sc-inc-gpc0(0)` and test `sc_gpc0(0)`. In HTTP mode every request is counted, including successive requests on one keep-alive connection — which is correct for a request-rate limit, and worth stating explicitly because candidates often assume the counter is per connection. ## Choosing the key `src` is the source address of the TCP connection as HAProxy sees it. That is right when HAProxy is the edge. It is wrong the moment something sits in front: - **Behind a CDN or another proxy** every request appears to come from a handful of addresses. One entry accumulates the whole site's traffic, crosses the threshold in seconds, and the deny rule takes the site down for everyone. The fix is to key on the forwarded client address — for example `http-request track-sc0 req.hdr_ip(X-Forwarded-For)` — but only where that header is set by infrastructure you control and cannot be supplied by the client, otherwise anyone can rotate the header and escape the limit entirely. If HAProxy receives the PROXY protocol from the upstream, `src` is already the real client and no header is involved. - **Behind NAT or CGNAT** many genuine users share one address, so a per-IP limit throttles a whole office. Per-account keys — an API key or authenticated user id from a header — are fairer where they exist. ## The response `http-request deny` returns 403 by default; `deny_status 429` is what a throttled client should receive so its own retry logic can react. Denying at the frontend means the request never reaches a backend, which is the point: the work is refused before it costs a connection to an application server. There are gentler alternatives once you have the counter. A `use_backend` on a slow or reduced backend degrades rather than blocks, and setting a header instead of denying lets you run the rule in observation mode first, watching what would have been rejected before you turn it on. Rolling out a new limit that way is what separates a limit that protects the service from one that pages you at 3am about your own biggest customer.

  • Why does the rate limit never fire if the deny rule is placed above the track rule?
    HAProxy evaluates `http-request` rules in order. With no tracking yet performed, `sc_http_req_rate(0)` resolves against an unbound counter and returns zero, so the condition is never satisfied. The configuration is valid and starts cleanly, which is exactly what makes it a durable production bug.
  • HAProxy sits behind a CDN. What changes about the key you track on?
    `src` becomes the CDN's egress address, so all traffic shares one entry and the limit trips for everyone. Track the forwarded client address instead — for example `req.hdr_ip(X-Forwarded-For)` — but only when that header is written by infrastructure you control, since a client-supplied header can be rotated to defeat the limit. With the PROXY protocol, `src` is already correct.
  • How would you roll out a new limit without risking a self-inflicted outage?
    Track and observe first: keep the tracking rule but replace the deny with a header or a log entry, then measure how many real clients would have been rejected at the proposed threshold. Sticky counters are readable in logs and on the runtime API, so you can size the threshold against actual traffic before it starts refusing anything.
  • Can one connection be tracked against two different tables?
    Yes — that is what the additional sticky counter slots are for. Track the source address in `sc0` and, say, an API key header in `sc1`, each against its own table, then write separate conditions against `sc_http_req_rate(0)` and `sc_http_req_rate(1)`. A second `track-sc0` on the same connection is ignored rather than replacing the first.

saying these in an interview costs you the question

  • Putting the deny rule before the track rule
  • Expecting deny to return 429 without deny_status
  • Thinking the counter increments per connection, not per request
  • Tracking src while sitting behind a CDN
  • Trusting a client-supplied X-Forwarded-For as the key

context