HAProxy’s HTTP Keep-Alive doesn’t actually make connections persistent for the client; it makes them persistent for the server.

Let’s see it in action. Imagine a client making multiple requests to your web server. Without Keep-Alive, each request would require a brand new TCP connection to be established. This involves the full TCP handshake (SYN, SYN-ACK, ACK), and then the HTTP request/response cycle. For every single request. That’s a lot of overhead, especially for small, frequent requests.

# Client's perspective, simplified:
# Request 1
SYN ->
<- SYN-ACK
ACK ->
GET /page1 HTTP/1.1
Host: example.com
...
<- HTTP/1.1 200 OK
Content-Length: 1000
...

# Request 2 (after connection closed)
SYN ->
<- SYN-ACK
ACK ->
GET /page2 HTTP/1.1
Host: example.com
...
<- HTTP/1.1 200 OK
Content-Length: 1500
...

With HTTP Keep-Alive enabled, the client and server agree to keep the TCP connection open after a request is served. This allows subsequent requests to be sent over the same, already established connection. The TCP handshake is skipped entirely for those follow-up requests.

# Client's perspective, simplified, with Keep-Alive:
# Request 1
SYN ->
<- SYN-ACK
ACK ->
GET /page1 HTTP/1.1
Host: example.com
Connection: keep-alive  # Client signals its intent
...
<- HTTP/1.1 200 OK
Content-Length: 1000
Connection: keep-alive  # Server agrees
...

# Request 2 (over the same connection)
GET /page2 HTTP/1.1
Host: example.com
Connection: keep-alive
...
<- HTTP/1.1 200 OK
Content-Length: 1500
Connection: keep-alive
...

HAProxy acts as a proxy in this scenario, and it needs to manage these persistent connections. When a client connects to HAProxy, and HAProxy in turn connects to a backend server, HAProxy can decide whether to reuse the connection to the backend server for subsequent requests from the same client. This is where HAProxy’s Keep-Alive configuration comes in.

Here’s a typical HAProxy configuration snippet for managing Keep-Alive:

frontend http_frontend
    bind *:80
    mode http
    default_backend web_servers

backend web_servers
    mode http
    balance roundrobin
    server web1 192.168.1.10:80 check inter 2000ms
    server web2 192.168.1.11:80 check inter 2000ms
    # HAProxy's default is to *not* reuse connections to backends.
    # We need to explicitly tell it to.
    option httpchk GET /health HTTP/1.1\r\nHost:\ www.example.com

The core lever you’ll pull is within the backend section. By default, HAProxy establishes a new TCP connection to the backend server for every request it forwards from a client. To enable persistent connections to your backend servers, you use option httpchk. This directive, while named for health checks, implicitly enables connection reuse for the backend servers. When HAProxy sees option httpchk, it will try to keep the TCP connection to the backend server open after it has finished proxying a request, rather than tearing it down. This is because it anticipates needing to send another request (the health check) to that server shortly.

Let’s break down the mental model:

  • Client -> HAProxy: HAProxy respects the Connection: keep-alive header sent by the client. If the client wants to keep the connection open, HAProxy will, up to its own connection limits.
  • HAProxy -> Backend: This is where HAProxy has control. By default, it closes the connection to the backend after each request. When you configure option httpchk, HAProxy becomes more "sticky" with its backend connections. It will keep the connection alive, making it available for the next client request that needs to be forwarded to that specific backend server. This is a powerful optimization.

The primary benefit of enabling this is reduced latency. Establishing a TCP connection is not instantaneous. It involves network round trips. By keeping connections alive between HAProxy and your backend servers, you eliminate this overhead for subsequent requests, leading to faster response times for your users. It also reduces the load on the backend servers, as they don’t have to constantly set up and tear down TCP connections.

A common point of confusion is that option httpchk is only for health checks. While that’s its primary purpose, it has this crucial side effect of enabling backend connection reuse. There isn’t a separate, explicit option http-keep-alive-backend directive. The mechanism relies on HAProxy’s internal logic for managing connections it expects to use again soon, and health checks are the prime example of that.

When you configure option httpchk, HAProxy will issue an HTTP request (as defined) to the backend server at regular intervals specified by inter. Crucially, after receiving the response to this health check, HAProxy will keep the TCP connection to that backend server open. This "warm" connection is then available for proxying actual client requests. If a client request arrives and HAProxy has an open, healthy connection to a suitable backend server, it will use that existing connection instead of establishing a new one.

The http-server-close directive in the backend section can also influence this. If you set http-server-close to a value greater than 0, HAProxy will close the connection to the backend server after that many seconds of inactivity. This helps prevent idle connections from consuming resources indefinitely. The default is typically 10 seconds.

The keepalive-timeout global setting is another related parameter. It controls how long HAProxy will keep idle client connections open. However, for backend connections, the option httpchk is the primary driver for keeping them alive proactively.

If you’ve configured option httpchk and are still not seeing connections being reused (which you can observe by monitoring TCP connections on your backend servers), ensure your health checks are actually succeeding. If the health check fails, HAProxy will tear down the connection to that backend server and won’t attempt to reuse it.

You’ll want to monitor the TCP connection count on your backend servers. If it’s significantly lower than the number of client requests you’re serving, and you’ve configured option httpchk, then you’re likely benefiting from connection reuse.

The next rabbit hole you’ll likely fall down is managing the timeout connect and timeout client settings in conjunction with keepalive-timeout to precisely tune how long HAProxy waits for backend connections to establish and how long it holds onto idle client connections.

Want structured learning?

Take the full Haproxy course →