HAProxy in TCP mode doesn’t actually proxy the TCP connection; it terminates the client’s connection and establishes a new one to the backend.

Let’s see HAProxy in action, proxying raw TCP connections. Imagine a simple TCP service running on port 5000 on a backend server 192.168.1.100. We want HAProxy to listen on 192.168.1.1:1234 and forward traffic to this backend.

Here’s a minimal HAProxy configuration:

global
    log /dev/log local0
    log /dev/log local1 notice
    daemon

defaults
    mode tcp
    timeout connect 5000ms
    timeout client 50000ms
    timeout server 50000ms

listen my_tcp_service
    bind 192.168.1.1:1234
    server backend1 192.168.1.100:5000 check

With this running, if you connect to 192.168.1.1:1234 using telnet or nc, HAProxy will establish a connection to 192.168.1.100:5000. You can verify this by running netstat -tulnp | grep 1234 on the HAProxy machine to see it listening, and then on the backend server, you’d see a new connection from the HAProxy server’s IP on port 5000.

The problem HAProxy in TCP mode solves is providing a single, stable entry point for services that might otherwise have dynamic IP addresses, or for load balancing multiple instances of a TCP service. It also allows for health checking of backend servers, ensuring traffic only goes to healthy instances.

Internally, when HAProxy is in mode tcp, it operates at Layer 4 of the OSI model. It doesn’t inspect the payload of the TCP packets. When a client connects to a bind address, HAProxy accepts that connection. Then, based on the server line, it initiates a new TCP connection to the specified backend server and port. All data received from the client is then immediately forwarded to the backend server through the new connection, and vice-versa. The check directive on the server line means HAProxy will periodically attempt to establish a TCP connection to the backend server on its specified port to verify its availability. If the connection fails, the server is marked as down, and HAProxy stops sending traffic to it.

The timeout connect parameter is crucial here. It defines how long HAProxy will wait to establish a connection to the backend server after it accepts the client connection. If this timeout is reached before a connection can be made, HAProxy will return a connection refused error to the client.

A common misconception is that HAProxy "tunnels" the TCP connection. It doesn’t. It actively terminates the client’s TCP connection and initiates a brand new one to the backend. This means the source IP address seen by the backend server will be the HAProxy server’s IP, not the original client’s IP, unless you’re using specific proxy protocols or network configurations to preserve that information.

The next logical step is understanding how to make HAProxy aware of the original client’s IP address when using TCP mode.

Want structured learning?

Take the full Haproxy course →