Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Choose the load-balancing algorithm and measure health checks

By updated

Improve this page

HAProxy logo

Checking a backend means asking HAProxy to verify at regular intervals that the server can really serve, and to remove it from the rotation when it can no longer do so. This lesson is for someone who already routes requests with HAProxy and wants traffic to avoid failing servers without needless failovers. It covers the load-balancing algorithms and the weights, the active health checks check and option httpchk, the inter, fall and rise thresholds, the difference between an open port and a ready service, then draining, which removes a server without cutting what it already serves. Unlike Nginx, which only detects a failure passively (see Nginx upstream: spread the load and remove failures), HAProxy checks actively.

What you will learn

The lesson goes from the choice of balancing to the measurement of a convergence.

  • Choose roundrobin or leastconn, weights and a backup server according to the need.
  • Declare an application-level health check with check, option httpchk, inter, fall and rise.
  • Tell apart an open TCP port, a live service and a ready service.
  • Remove, drain and reinstate a server, then read HAProxy's counters to prove it.
  • Verify that an outage shorter than the threshold triggers no failover.

Prerequisites

The lesson assumes you can write an HAProxy frontend and backend and query its socket.

How does HAProxy spread traffic across servers?

HAProxy picks a server of the backend according to the balance algorithm and the servers' weights: roundrobin rotates requests in proportion to the weights, leastconn sends to the server that has the fewest active connections. Neither is better in the absolute: the right choice depends on the duration of requests. And it must be written: without a balance directive, HAProxy 3.4 applies random, which draws a server at random (it has been the default since 3.3).

With roundrobin, each server receives a request in turn, respecting the weights. It is the right choice when requests are short and uniform. With leastconn, HAProxy compares the number of open connections and picks the least loaded: it suits long or very uneven requests (downloads, sessions, streams), where a round robin would let one server pile up slow connections.

Always write balance yourself, roundrobin included. With random, there is no round robin: a demonstration that counts on "one request at one server, the next at the other" can only be checked on average. For this reason, the lab's starting backend declares balance roundrobin in black and white, and that is what makes the alternation predictable.

Weights declare the relative capacity. weight 3 against weight 1 means the first takes three requests out of four when healthy. The lab prescribes weights of 3 and 1, and measures the share taken on a sample. A server marked backup receives nothing as long as all the ordinary servers are down: it is the last resort, not one more server.

backend api
balance leastconn
option httpchk GET /sante
http-check expect status 200
default-server inter 2s fall 3 rise 2
server api1 127.0.0.1:9101 check weight 2
server api2 127.0.0.1:9102 check weight 1
server api3 127.0.0.1:9103 check backup

This example is deliberately different from the lab's: another algorithm, also declared explicitly, other thresholds, a backup server. default-server sets the settings shared by all the servers of the backend, which avoids repeating them. The lab's acceptance criteria prescribe their own values: read them in /opt/pub-haproxy-sante/recette.txt rather than copying these.

What happens when no health check runs?

Without a health check, a failed server stays in the rotation: HAProxy keeps sending it its share of the traffic, and clients receive errors. The lab starts from this state, with equal weights, with no check and no option httpchk.

Observe it before writing anything. The scenario does it by setting the state file of the beta server to casse, then sending six requests:

Terminal window
echo casse > /opt/pub-haproxy-sante/etat-beta
for i in $(seq 6); do curl -s -o /dev/null -w '%{http_code} ' http://127.0.0.1/; done
echo pret > /opt/pub-haproxy-sante/etat-beta

With equal weights and under balance roundrobin, HAProxy sends one request out of two to beta, which answers a 500 error: about half of the responses are failures, while the beta process is still running. Putting the state back to pret afterwards avoids leaving the service broken. The point to keep: as long as HAProxy does not know beta is sick, it can do nothing.

Does an open port prove the service is ready?

No: a bare check only verifies that a TCP connection opens, which a service that has lost its database also does. You have to tell three levels of health apart, and each answers a different question.

LevelQuestion askedExample health checkWhat it misses
Open port (TCP)Is the process listening?check aloneA live service that is unable to answer
Liveness (alive)Does the process answer in HTTP?option httpchk GET /vivantA failed dependency: the process answers, but serves nothing useful
Readiness (ready)Can the service serve?option httpchk GET /readyNothing essential, if /ready checks what the service needs

In the lab, each backend exposes /ready. The casse state makes the health check get a 503 and useful requests a 500: this is the situation a readiness check exists to cover, that of a live service whose dependency has failed. A bare check would keep seeing the port open and would leave beta in the rotation. Kubernetes contrasts the same notions of liveness and readiness: see Kubernetes probes: liveness, readiness, startup without false positives (in French).

The lab's check requires the health check to be at layer 7 (the application layer, that of the HTTP protocol, as opposed to layer 4, that of the TCP connection), which HAProxy reports through the check_status field of show stat. The L7OK value means the health check read an acceptable application response; L4OK only means that a port accepted a connection. The two can be told apart on reading, and a bare check will never give better than L4OK.

A health check can also go further than a status: http-check expect verifies the status or the content of the response (http-check expect status 200, or a string expected in the body). check-ssl forces the health check to use TLS, whether or not the server is declared ssl for traffic: it is mostly used when the health check targets another port or addr and so does not inherit the server's TLS. For an HTTPS health check, the SNI (the server name announced in the TLS handshake) is derived from the Host header by default (sni-auto, since 3.3). These variants are not replayed in the lab, which checks in plain HTTP.

How do you tune inter, fall and rise?

inter is the interval between two health checks, fall the number of consecutive failures before removing a server, rise the number of consecutive successes before reinstating it. These three numbers set the window during which an outage goes unnoticed, and the one during which a recovered server stays out.

The calculation is direct. With inter 1s, fall 5 and rise 3 (the values of the lab's acceptance criteria), a server is removed after about five seconds of continuous failure, and reinstated after about three seconds of continuous success. These bounds show up in the test: it waits fall × inter + 8 seconds to observe the removal, and rise × inter + 12 to observe the return, with a margin added each time.

Why not set fall 1 to react faster? Because a health check tuned too short turns every hiccup into a failover, and the failover costs more than the hiccup: all the new traffic shifts onto the remaining server, and the load it takes can bring it down in turn. Only this new traffic is moved. The manual says of the on-marked-down shutdown-sessions option: "Actions are disabled by default". From this we infer that, without it, HAProxy does not cut the connections already established to a server it marks DOWN; the manual does not state it in a general sentence. A well-tuned threshold therefore removes what has really failed and ignores what does not last.

Sequence between HAProxy and the beta server. The server is ready, health check 1 receives 200. Then /ready returns 503: health checks 2 and 3 fail, which makes two failures out of the five needed. The outage ends, health check 4 receives 200. No transition to down took place, the server never left the rotation.beta serverHAProxystate ready,/readyreturns 200/ready startsreturning 503the failureends beforefall failuresno downtransition, theserver neverleft therotationprobe 1, answer 200probe 2, 503 (failure 1of fall)probe 3, 503 (failure 2of fall)probe 4, answer 200
An outage shorter than fall consecutive failures leaves failed checks behind but removes nothing. Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

Tune inter, fall and rise according to what your service tolerates. A short inter detects fast, but sends more health checks: if there are many, each one loads the service even in the middle of an outage. An expensive health check, which queries the database on every call, can itself worsen an incident. HAProxy offers two variants, not to be confused: fastinter replaces inter during a transition (a server going down after a failure, or coming back up, or not yet checked), to decide faster; downinter replaces inter when the server is fully DOWN, useful to slow the health checks of a server that is already dead. Without them, inter applies everywhere (2000 ms by default, fall at 3 and rise at 2 if nothing is specified).

How do you read HAProxy's counters?

show stat, through the admin socket, returns one line per server with its current state and transition counters; it is the counters, not the state, that tell what happened. The columns useful here are status, check_status, weight, chkfail and chkdown.

chkfail counts the failed health checks while the server is UP (once DOWN, it no longer moves), chkdown the down transitions, that is, the times the server went from UP to DOWN. A state alone does not distinguish two opposite stories: a server UP ten seconds after a short outage may never have been removed, or may have been removed then put back. With rise 3 and inter 1, both give UP at the same instant. Only chkdown tells which of the two happened: that is exactly the difference between a stable health check and one that oscillates.

The lab's test reads the columns by their name, in the first line of the output, which starts with # pxname,svname,.... A shift in a column added by a version of HAProxy would break a reading by position. You can do the same:

Terminal window
echo 'show stat' | sudo socat -t3 stdio /run/haproxy/admin.sock |
awk -F, 'NR==1 { sub(/^# /, ""); for (i=1; i<=NF; i++) col[$i]=i; next }
$col["pxname"]=="applicatif" && $col["svname"]!="FRONTEND" && $col["svname"]!="BACKEND" {
print $col["svname"], $col["status"], $col["check_status"], $col["chkfail"], $col["chkdown"] }'

This script prints, for each server of the applicatif backend, its name, its state, the type of health check and the two counters. It has not been replayed on a machine: if a column is missing, check the exact name in the first line of your output. The status values to know are UP, DOWN, DRAIN and MAINT.

How do you remove an unready backend, and measure it?

Making /ready fail on a live server turns it DOWN after fall failures, and the client no longer sees an error once convergence is reached. This is the gesture to prove, and the proof is a counted down transition, not an observed state.

The demonstration follows the outage without stopping the process. You set the state file of beta to casse: the process runs, /ready returns 503, the health check fails. HAProxy counts the failures, then at the fall-th it removes beta. The test then requires three things: a status other than UP in the planned window, one more down transition in chkdown (more than one means the server went down, came back, then went down again, so the thresholds oscillate), and no more requests to beta after convergence, with no error on the client side.

The return is measured the same way. You restore the pret state: the health check starts succeeding again, and at the rise-th consecutive success the server returns to UP. What happens in between is the remaining capacity: with weights of 3 and 1, removing the weight-3 server leaves the weight-1 server carrying all the traffic. Ask yourself the question before the outage, not during it: does the remaining server hold the load? If not, maxconn on the server line caps its simultaneous connections and timeout queue sets how long a request waits in the queue before failing: requests wait rather than saturate the server.

To reinstate smoothly, slowstart raises the weight of a returning server gradually: it takes back its share over the duration you set rather than all at once. This setting is not replayed in the lab; test it with a bounded load before keeping it.

Why should a short outage remove nothing?

A health check is well tuned when it removes a server that has really failed and ignores a dip shorter than its threshold; the second half's test is the counter-test that stops you accepting fall 1. Without it, a threshold that is too short would pass the previous measurement with flying colors.

The lab's test takes beta down for (fall - 2) × inter seconds, that is 3 seconds with the acceptance-criteria values, then restores it. Enough for health checks to fail, not enough to reach the threshold. It then requires that chkfail has increased, proof that the outage was seen, and that chkdown has not moved, proof that no down transition took place. If chkfail does not increase, the health check is slower than the dip or does not read /ready: the test measured nothing.

Read this counter-test as a property of your configuration, not as a lab trick. In operation, service restarts, garbage-collector pauses and network micro-cuts can last a few seconds. A health check that removes at each of them makes the backend oscillate, and each oscillation abruptly shifts new traffic to the other servers (and also cuts established connections if on-marked-down shutdown-sessions is set).

What do a stop, an HTTP failure and slowness change?

A health check tells three failures apart: a stopped process refuses the connection, an HTTP failure answers a bad status, slowness exceeds the health check's timeout; each leaves a different trace in check_status. Knowing them avoids looking in the wrong place.

A stopped server no longer answers at all: the health check fails on the TCP connection, and check_status carries a layer-4 value. An HTTP failure, like the 503 of /ready in the lab, lets the connection establish but the response is not acceptable: the value is layer 7, with an unexpected status. Slowness that exceeds timeout check (or inter when timeout check is not set) is counted as a timeout failure. None of the three has the same remedy: restart a process, fix a dependency, or tune a timeout.

To test these cases without damaging the machine, bound the test rate. Send a finite number of requests (twenty, forty), with curl --max-time, and never an endless loop. The lab provides a client-side utility that counts responses per server on a fixed sample, which makes the measurement reproducible and prevents saturating the service you are diagnosing.

How do you drain a server without cutting what it serves?

Draining a server stops new assignments but lets already established connections finish, whereas stopping the process cuts them; this is what allows maintenance without a visible outage. The difference between draining and stopping is what the lab measures on an open stream.

The state is driven through the admin socket:

Terminal window
echo 'set server applicatif/beta state drain' | sudo socat -t3 stdio /run/haproxy/admin.sock
echo 'set server applicatif/beta state ready' | sudo socat -t3 stdio /run/haproxy/admin.sock

A drained server appears as DRAIN in show stat. New requests no longer reach it, except those that a session persistence (the mechanism that sends the same client back to the same server) ties to it. A long stream, opened before the draining, continues to its end. The maint state (or the disable server command) removes the server from the rotation and also stops its health checks; it does not let through the persistent sessions that drain still accepts. Returning to ready puts the server back in the rotation, and the health check remains the judge of its state.

The test chains two drainings: it first drains alpha, so that a long stream can only land on beta, deterministically, without opening at random and retrying. Then it restores alpha, drains beta, and measures two things. Forty new requests no longer reach beta and produce no error; and the stream established on beta keeps receiving its lines, with no error message. If the stream stops as soon as the draining starts, you have not drained, you have stopped.

Practice

The lab gives you two backends that serve the same application behind HAProxy, with no health check and no weights. You declare the /ready health check and the thresholds prescribed by the acceptance criteria, the weights of 3 and 1, then you show that you remove a server without cutting what it serves. The validation measures four things: the share of each server and a layer-7 health check, the removal of a live but unready backend within the computed window, the absence of removal on an outage shorter than the threshold, and a draining that stops new assignments without cutting the established stream.

Key points

  • roundrobin suits short, uniform requests, leastconn long or uneven ones, and without balance HAProxy 3.4 applies random: you write the algorithm; weights set each server's share, backup only serves when all the others are down.
  • A bare check only verifies an open port (L4OK); an option httpchk health check reads an application response (L7OK) and covers the case of a live service whose dependency has failed.
  • inter, fall and rise set the detection and return window: about fall × inter seconds to remove, rise × inter to reinstate.
  • A fall that is too short turns every hiccup into a failover: the counter-test requires chkfail to rise and chkdown not to move on an outage shorter than the threshold.
  • A failover shifts new traffic to the other servers; connections already established are not cut by a transition to DOWN (inferred from the manual), unless on-marked-down shutdown-sessions is set.
  • The chkfail and chkdown counters of show stat tell what happened; an UP state does not say whether the server went down and came back. Columns are read by name.
  • Before the outage, compute the remaining capacity: the server that remains must hold the load; maxconn and timeout queue bound the queue.
  • Draining (set server ... state drain) stops new assignments without cutting what is established; stopping the process cuts established streams.
  • Test with a bounded rate: a finite number of requests, never an endless loop on an already fragile service.

Knowledge check

Check that you have grasped the essentials of this guide. The questions cover only what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

8 questions
8 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn