skip to content

HAProxy

HAProxy is the load balancer I reach for when I need one hardened daemon doing both TCP (L4) and HTTP (L7) proxying with predictable, single-threaded-per-core performance. Interviewers use it to see whether I can read a real proxy config, keep a pool healthy under partial failure, and defend a backend from abusive traffic without bolting on a WAF.

on this pageshow

questions

17

Walk through the shape of an HAProxy configuration file: what do the global, defaults, frontend and backend sections each configure, and what does a listen section do instead?

level: juniorimportance: must knowfreq 74%

answer

  1. four section kinds, two of them proxies
  2. process settings versus traffic settings
  3. one side binds, the other holds servers
  4. a template that only applies downward
  5. listen = bind and server together

basics

~20 s

HAProxy config is split into global (process-wide settings), defaults (a template inherited by every proxy declared after it), frontend (a bind address that classifies incoming traffic), and backend (the server pool). A listen section fuses a frontend and backend into one block.

solid answer

~40 s

An HAProxy config is a list of sections. `global` configures the process itself — privileges, `daemon`, thread count, process-wide `maxconn`, logging targets, default TLS settings. `defaults` is not a proxy at all; it is a template whose settings are inherited by every `frontend`, `backend` and `listen` declared **after** it, which is why a second `defaults` block silently changes everything below it and nothing above. A `frontend` owns the `bind` line and decides where traffic goes: `acl` definitions, `http-request` rules, `use_backend` and `default_backend`. A `backend` owns the pool — `server` lines, `balance`, health checks, persistence. A `listen` section simply combines both: it can hold `bind` and `server` lines together, which is handy for a simple one-to-one TCP proxy. Validate any of it with `haproxy -c -f /etc/haproxy/haproxy.cfg` before reloading.

go deeper

for a junior

Be able to read a small haproxy.cfg out loud and say which section each line belongs to. The one-liner to have ready: the frontend binds and decides, the backend holds the servers.

for a middle

Explain that defaults is a positional template, not a merge, and that a second defaults block resets it for everything below. Know that global never contains bind or server lines.

for a senior

Show that you validate with haproxy -c before a reload and that you standardise mode and the timeout family in defaults so no proxy can ship without them. Say why you split listen into frontend/backend once routing appears.

for a principal

Own the config layout for a fleet: generated versus hand-edited, named defaults sections for mixed HTTP and TCP proxies, unique proxy naming, and where the syntax check sits in the pipeline that produces the file.

## Why the file is shaped this way HAProxy separates *how the process runs* from *how traffic is received* from *where traffic goes*. Those three concerns map onto `global`, `frontend` and `backend`, with `defaults` acting as a template so you do not repeat the same twelve lines in twenty proxies. Reading a config fluently is mostly knowing which concern each keyword belongs to. ``` global maxconn 20000 log /dev/log local0 user haproxy group haproxy daemon defaults mode http timeout connect 5s timeout client 30s timeout server 30s frontend public bind :80 acl is_api path_beg /api/ use_backend api if is_api default_backend web backend web balance roundrobin server web1 10.0.0.11:8080 server web2 10.0.0.12:8080 backend api server api1 10.0.0.21:9000 ``` ## global — the process `global` describes the running program, not traffic. It holds privilege dropping (`user`, `group`), whether to background (`daemon`), the threading model (`nbthread`), logging destinations (`log`), default TLS material such as `ssl-default-bind-ciphers` and `ssl-default-bind-options`, and a process-wide `maxconn` that caps concurrent connections across every proxy. It never contains `bind` or `server` lines — if you find yourself wanting to put a listening address here, you want a frontend. ## defaults — a template, not a proxy `defaults` accepts most of the same keywords a frontend or backend accepts, but it never serves traffic. Its settings are copied into each proxy declared **after** it in the file. Two consequences trip people up. First, it is positional: a `defaults` block placed at the bottom of the file affects nothing. Second, declaring a second `defaults` block replaces the template for everything below it — the two are not merged, so a `backend` after the second block does not inherit the first block's timeouts. From HAProxy 2.4 you can name defaults sections and select one explicitly, for example `defaults tcp-common` followed by `backend db from tcp-common`, which makes mixed HTTP/TCP configs far less accident-prone. Almost every real config puts `mode`, the `timeout` family and `option httplog` here, because forgetting a timeout in a proxy is a production outage waiting to happen. ## frontend — where traffic arrives and gets classified A `frontend` must have at least one `bind` line, which is the socket: `bind :80`, `bind :443 ssl crt /etc/haproxy/certs/site.pem`, optionally with `alpn h2,http/1.1` or `accept-proxy`. Everything else in a frontend is about *classification and decision*: `acl` statements that name a condition, `http-request` rules that inspect or rewrite, `use_backend` rules that route, and `default_backend` as the fallback. A frontend has no idea what a server is; if no `use_backend` matches and there is no `default_backend`, HAProxy has nowhere to send the request and answers 503. ## backend — the pool A `backend` has no `bind`. It holds `server` lines (`server web1 10.0.0.11:8080 check maxconn 100`), the `balance` algorithm, health-check directives, persistence, and its own `http-request`/`http-response` rules which run after the frontend's. Backends are referenced by name, and names must be unique across all proxies in the file. ## listen — both at once `listen` is a frontend and a backend in one section, so it can carry `bind` and `server` lines together: ``` listen mysql mode tcp bind :3306 server db1 10.0.0.31:3306 ``` There is nothing second-class about it; it is the same machinery with one name. It reads well for a simple pass-through where one listening port maps to one pool, and reads badly as soon as you want several backends behind one port — at that point split it, because only a frontend/backend pair lets several frontends share a backend or one frontend fan out. ## Checking it `haproxy -c -f /etc/haproxy/haproxy.cfg` parses the file and reports errors without starting the proxy. Run it in CI and before every reload; most of the mistakes above (an unknown backend name, a `server` line in the wrong section, a proxy with no timeouts) are caught there rather than in production.

  • Where would you put the mode and timeout settings, and why does it matter that defaults is positional?
    Put `mode` and the `timeout connect`/`client`/`server` family in `defaults` so no proxy is left without them. It matters because a `defaults` block applies only to proxies declared after it, and a second `defaults` block replaces the template rather than merging with it — so moving a backend above or below a defaults section silently changes its behaviour.
  • A frontend has no default_backend and no use_backend rule matches. What does the client get?
    A 503. The frontend parsed the request fine but has no destination, so HAProxy generates the error itself. In the logs the termination state shows the request never reached a server. Always give a frontend an explicit `default_backend`, even if it only serves a static error or a maintenance page.
  • When would you split a listen section into a separate frontend and backend?
    As soon as more than one destination or more than one entry point is involved: several backends selected by ACLs behind one bind, or two frontends (say plain HTTP and HTTPS) sharing one pool. A `listen` block hard-couples one bind to one pool, so it stops paying off the moment routing appears.

saying these in an interview costs you the question

  • Thinks defaults applies to proxies declared above it
  • Puts server lines in a frontend or bind in a backend
  • Believes global holds listening addresses
  • Says listen is deprecated or behaves differently from frontend plus backend
  • Cannot name which section default_backend belongs to

context

open as a page

In HAProxy, a backend contains the line `server app1 10.0.0.1:8080 check`. What does the `check` keyword probe on its own, and how do `option httpchk` and `http-check expect` change what HAProxy accepts as a healthy server?

level: juniorimportance: must knowfreq 74%

basics

~20 s

HAProxy's bare check keyword only opens a TCP connection to the server port. option httpchk makes the probe send an HTTP request instead, and http-check expect narrows success from any 2xx or 3xx reply to a specific status, regex or body string.

open as a page

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%

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.

open as a page

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%

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.

open as a page

You need to take one server out of an HAProxy backend for a deploy without dropping in-flight requests and without reloading HAProxy. What does the Runtime API let you do, what must the config expose for it, and how do the DRAIN and MAINT states differ?

level: seniorimportance: must knowfreq 56%

basics

~20 s

HAProxy's Runtime API, exposed through a stats socket at admin level, accepts set server <backend>/<server> state drain to stop new sessions while existing ones finish. MAINT forces the server fully out and stops its health checks; DRAIN keeps checking and still honours persistence.

open as a page

An HAProxy backend contains the lines `stick-table type ip size 1m expire 30m` and `stick on src`. What do those two lines make the backend do with a returning client, and what happens once an entry expires?

level: juniorimportance: should knowfreq 45%

basics

~20 s

HAProxy keeps one in-memory entry per client IP recording which server that IP was sent to, and routes the same IP back to that server on later requests. After 30 minutes with no match the entry is purged and the balance algorithm chooses again.

open as a page

In HAProxy, what changes when a proxy runs in `mode tcp` instead of `mode http`, and what must be true about the mode of a frontend and the backends it routes to?

level: middleimportance: should knowfreq 58%

basics

~20 s

In mode http HAProxy parses each request, so HTTP fetches, http-request rules and per-request balancing work. In mode tcp it relays bytes, balancing once per connection with only L4 and TLS-hello fetches. A frontend and the backends it uses must share the same mode or the config is rejected.

open as a page

A `systemctl reload haproxy` runs `haproxy -f haproxy.cfg -p /run/haproxy.pid -sf $(cat /run/haproxy.pid)`. What does the `-sf` flag do to the old process and its in-flight connections, and what can still be lost across that reload?

level: middleimportance: should knowfreq 50%

basics

~20 s

HAProxy's -sf means soft-finish: the new process takes over the listening sockets, then tells the listed old processes to stop accepting and finish their existing sessions before exiting. Runtime state, server health history and in-memory counters do not carry across.

open as a page

An HAProxy server line reads `server app1 10.0.0.1:8080 check inter 2s fall 3 rise 2`. What does each of `inter`, `fall` and `rise` control, roughly how long can it take HAProxy to notice that server has died, and what do `fastinter` and `downinter` add?

level: middleimportance: should knowfreq 62%

basics

~20 s

In HAProxy, inter is the interval between probes, fall the number of consecutive failed checks that mark a server DOWN, and rise the consecutive successes that bring it back. Detection costs roughly fall times inter, which fastinter shortens once a check has already failed.

open as a page

In HAProxy, what do the `size`, `expire` and `store` parameters of a `stick-table` line control, and what happens to new clients once the table is full?

level: middleimportance: should knowfreq 48%

basics

~20 s

size is the maximum number of entries, expire the idle timeout per entry, and store the list of counters each entry carries, with the measurement window declared inside the counter. When the table is full HAProxy purges the oldest entries to make room, unless nopurge is set.

open as a page

Applications behind an HAProxy tier log every request as coming from the proxy's own address. Which HAProxy settings restore the real client address, and what breaks when the two ends disagree about the PROXY protocol?

level: seniorimportance: should knowfreq 50%

basics

~20 s

HAProxy opens its own connection to the server, so the source address is the proxy's. In HTTP mode option forwardfor adds the client address as a header; for TCP or TLS passthrough the PROXY protocol carries it, via send-proxy on the server line and accept-proxy on a receiving bind.

open as a page

An HAProxy backend keeps dispatching to a server whose /healthz probe still returns 200 while real requests to it fail or return 5xx. Which HAProxy server keywords let live traffic itself mark that server down, and what has to be in place for them to take effect?

level: seniorimportance: should knowfreq 42%

basics

~20 s

HAProxy's observe server keyword watches real traffic instead of probes: observe layer4 counts connection errors, observe layer7 also counts 5xx and malformed responses. error-limit sets how many errors trigger the on-error action, and active checks must be enabled for it to work.

open as a page

Two HAProxy nodes sit behind a round-robin DNS record and each enforces a per-source-IP request limit using its own stick table. What goes wrong, and what does adding an HAProxy `peers` section change?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Each node counts only the traffic it sees, so a client spread across both nodes gets roughly double the intended limit, and a client that moves nodes loses its stickiness. A peers section replicates table entries between nodes asynchronously, giving one shared view rather than two independent ones.

open as a page

What does HAProxy do with a request when every server in a backend has reached the `maxconn` set on its `server` line, and how does that limit differ from `maxconn` in the global section?

level: seniorimportance: nice to knowfreq 34%

basics

~20 s

HAProxy holds the request in the backend's queue until a server slot frees, bounded by timeout queue, after which the client gets 503. Server-line maxconn caps concurrent connections to one server; global maxconn caps them for the whole process.

open as a page

HAProxy's default HTTP log line ends each request with a four-character termination state such as `sH--` or `SC--`. What do the first two characters encode, and what do `sH`, `sQ`, `cD` and `SC` each tell you about a failed request?

level: seniorimportance: nice to knowfreq 34%

basics

~20 s

In HAProxy logs the first character says who or what ended the session and the second says which phase it was in. sH is a server-side timeout waiting for response headers, sQ a timeout in the queue, cD a client timeout during data transfer, and SC a server refusing or resetting the connection.

open as a page

Once an HAProxy stick-table counter marks a client as abusive, you can respond with `http-request deny`, `http-request tarpit` or `http-request silent-drop`. What does each do to the client and to your own resources?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

deny answers immediately with an error and frees the connection, tarpit holds the request for the tarpit timeout before answering and ties up a session slot meanwhile, and silent-drop discards the connection without telling the client, costing HAProxy least but leaving state on every device in between.

open as a page