skip to content

In an HAProxy frontend with several `use_backend` rules and a `default_backend`, how does HAProxy decide which backend a request goes to, and how do multiple ACLs in one condition combine?

level: middleimportance: must knowfreq 66%

answer

  1. ACLs name; rules decide
  2. evaluated top to bottom
  3. the first true one ends it
  4. space means AND, || means OR
  5. patterns on one line are OR-ed

basics

~20 s

HAProxy evaluates use_backend rules top to bottom and the first one whose condition is true wins; if none match it uses default_backend. Space-separated ACL names in a condition are AND-ed, || is OR, and ! negates.

solid answer

~40 s

An `acl` line only *names* a condition — it decides nothing. The routing happens in the `use_backend` rules, which HAProxy evaluates in the order they appear in the frontend; the first rule whose condition is true selects the backend and the rest are skipped. If nothing matches, `default_backend` applies, and with no `default_backend` the client gets a 503. Inside one condition, space-separated ACL names are AND-ed (`use_backend api if host_api path_api`), `||` means OR, `!` negates a single ACL, and `unless` inverts the whole condition. In the other direction, several patterns on one `acl` line are OR-ed, and two `acl` lines sharing a name are merged into one OR-ed ACL. So rule *order* is what you tune; ACL declaration order is irrelevant.

go deeper

for a junior

Know that an acl line only names a condition and that use_backend is what actually routes, with default_backend as the fallback. Be able to point at the rule a given request would hit.

for a middle

Explain first-match-wins ordering and the AND/OR rules precisely: spaces AND, || ORs, patterns on one acl line OR, same-named acl lines merge. Be ready to spot a broad rule shadowing a narrow one.

for a senior

Diagnose misrouting by reading rules in order rather than by intent, use the access log's frontend and backend fields to confirm, and know that frontend http-request rules run before selection so normalisation belongs there.

for a principal

Decide when a hand-written rule list should become a map-driven lookup, and own the consequences: routing that no longer reads from the config, a data file with its own reload path, and 503s for names with no matching backend.

## ACLs name conditions; rules make decisions HAProxy separates the test from the action. An `acl` statement binds a name to a fetch plus a pattern: ``` frontend public bind :80 acl host_api hdr(host) -i api.example.com acl path_v2 path_beg /v2/ acl is_static path_beg /img/ /css/ /js/ use_backend api_v2 if host_api path_v2 use_backend api_v1 if host_api use_backend statics if is_static default_backend web ``` Nothing routes until a rule references the name. The fetch (`hdr(host)`, `path_beg`, `src`, `method`, `url_param`, `ssl_fc`) says what to look at; the pattern says what counts as a match; `-i` makes the match case-insensitive, which matters for host names and rarely for paths. ## First match wins, and order is the whole game `use_backend` rules are evaluated top to bottom. The first rule whose condition is true selects the backend and evaluation stops — later rules are never consulted, even if they also match. That is why the example puts the narrower `host_api path_v2` rule above the broader `host_api` rule. Swap them and every `/v2/` request lands in `api_v1`, with no error and no warning; the config is perfectly valid, it just routes wrongly. When a request matches nothing, `default_backend` catches it. With no `default_backend` and no match, HAProxy has no destination and returns 503 itself. ACL *declaration* order, by contrast, does not matter at all. You can declare every ACL at the top of the frontend and the rules below them; the ACL is evaluated lazily when a rule references it. ## How conditions combine Within a condition: - **Space = AND.** `if host_api path_v2` requires both. - **`||` = OR.** `if host_api || host_legacy` requires either. - **`!` negates one ACL.** `if host_api !is_static`. - **`unless` inverts the whole condition.** `use_backend web unless host_api` is the same as `if !host_api`. Within an ACL: - **Several patterns on one line are OR-ed.** `acl is_static path_beg /img/ /css/` matches either prefix. - **Two `acl` lines with the same name are merged and OR-ed.** Declaring `acl host_api hdr(host) -i api.example.com` twice with different values gives one ACL that matches either. The common mistake is assuming the second rule — that repeating a name AND-s the tests — which produces an ACL that matches far more traffic than intended. ## Anonymous inline ACLs You do not have to name a condition. Braces declare one inline: ``` use_backend admin if { src 10.0.0.0/8 } { path_beg /admin/ } http-request redirect scheme https code 301 unless { ssl_fc } ``` Same semantics: two brace groups side by side are AND-ed. Inline ACLs read well for a one-off test and badly for anything reused, since a named ACL documents intent and can be referenced from several rules. ## Where this sits in request processing In a frontend, `http-request` rules run *before* backend selection. So a header you set with `http-request set-header` in the frontend is visible to the ACLs in a `use_backend` condition, and to the backend's own rules afterwards. The order inside the frontend is: frontend `http-request` rules, then `use_backend`/`default_backend`, then the backend's `http-request` rules. This is why normalising something — lower-casing a host, stripping a prefix with `http-request replace-path` — is done in the frontend if you want to route on the normalised value. ## Dynamic backend names For large fan-outs, writing one rule per tenant does not scale. `use_backend` accepts a sample expression, commonly through a map file: ``` use_backend %[req.hdr(host),lower,map(/etc/haproxy/hosts.map,web)] ``` The resolved string must be the name of a backend that exists in the config; if it does not, the request fails with 503. That trade — one line instead of hundreds, at the cost of a routing decision you cannot read from the config alone — is the usual reason teams reach for maps only once the rule list gets long. ## Debugging When traffic lands in the wrong pool, read the rules in order and find the first one that matches, rather than looking for the rule you *meant* to fire. The access log records the chosen frontend and backend, which makes it quick to confirm whether the wrong rule fired or the ACL itself never matched.

  • Two acl lines in an HAProxy frontend share the same name but test different host values. What does that name now match?
    Either value. Same-named `acl` lines are merged into a single ACL whose patterns are OR-ed, exactly as if the patterns had been written on one line. Candidates often expect an AND, which would make the ACL match nothing; if you really need both tests, give them different names and put both names in the condition.
  • How would you route on a header that your own frontend rewrites before routing?
    Put the `http-request` rewrite in the frontend. Frontend `http-request` rules run before `use_backend` evaluation, so an ACL in a routing condition sees the rewritten value. Rules placed in the backend run after selection and therefore cannot influence it.
  • When would you replace a long list of use_backend rules with a map-based expression?
    When the rules are a flat name-to-backend lookup — per-tenant or per-host fan-out — and the list is long enough that ordering mistakes become likely. `use_backend %[req.hdr(host),lower,map(...)]` turns it into one line plus a data file you can reload. The cost is that the routing table no longer lives in the config, and an unmatched name yields 503.

saying these in an interview costs you the question

  • Thinks the most specific matching rule wins rather than the first
  • Believes ACL declaration order affects routing
  • Assumes repeating an ACL name AND-s the tests
  • Says a request with no matching rule is dropped rather than sent to default_backend
  • Puts a rewrite in the backend and expects it to affect backend selection

context