HAProxy vs chproxy for ClickHouse: Which Load Balancer Should You Use?
If you run more than one ClickHouse node, sooner or later you need something in front of them. A single application talking directly to one node is a single point of failure, and once you have replicas, you want reads spread across them instead of hammering one server.
Two tools come up constantly for this job: HAProxy and chproxy. They solve overlapping problems in very different ways, and picking the wrong one leads to surprises in production — usually the "all my queries end up on one node" kind.
This post explains what each tool actually does, when one beats the other, how to set them up, and the handful of parameters that matter.
First, the one thing that decides everything: ClickHouse has two interfaces
Before comparing proxies, you have to understand how clients talk to ClickHouse, because that single fact determines which proxy makes sense.
ClickHouse exposes two protocols on two different ports:
- HTTP interface — port
8123. Used bycurl, the JDBC/ODBC HTTP drivers, most language clients in HTTP mode, and dashboards like Grafana. - Native TCP protocol — port
9000. Used byclickhouse-client, the native Go/Python drivers, and inter-service traffic that wants the fastest, most compact protocol.
(There are more ports — 8443 for HTTPS, 9440 for native TLS, 9009 for inter-server replication — but 8123 and 9000 are the two you load balance.)
Here is the crucial part:
- The HTTP interface is stateless per request. Each query is a self-contained HTTP request. A proxy can send request #1 to node A and request #2 to node B — true per-query balancing.
- The native protocol is a stateful TCP connection. A client opens a connection and reuses it for many queries. A Layer-4 (TCP) proxy picks a backend when the connection opens and is stuck with it until the connection closes.
This is the whole game. chproxy only speaks HTTP. HAProxy speaks both, but with an important limitation on the native protocol. Keep this in mind as we go.
What is chproxy?
chproxy is an HTTP proxy and load balancer built specifically for ClickHouse. It is a community project (not an official ClickHouse product) written in Go, distributed as a single binary with a YAML config file.
Because it understands that it is sitting in front of ClickHouse, it can do things a generic proxy cannot:
- Per-query load balancing across nodes using a round-robin + least-loaded strategy, automatically skipping nodes that recently failed health checks.
- Per-user limits — cap concurrent queries, execution time, and requests per minute for a given user.
- Request queueing — when a user hits their concurrency limit, chproxy can queue extra requests instead of rejecting them outright, which smooths out bursts.
- Response caching for identical queries.
- User mapping and multi-cluster routing — map an incoming user to a specific cluster and a specific ClickHouse user, so your app never sees real ClickHouse credentials.
- Prometheus metrics and config reload without a restart (send
SIGHUP).
The catch: it works only over the HTTP interface (port 8123). If your clients use clickhouse-client or a native driver on port 9000, chproxy is not for that traffic.
Minimal chproxy setup
Grab a precompiled binary from the releases page (or build from source with Go), then write a config file:
# config.yml
server:
http:
listen_addr: ":9090"
allowed_networks: ["10.0.0.0/24"] # only your app subnet may connect
users:
- name: "app"
password: "app-secret"
to_cluster: "analytics"
to_user: "default"
max_concurrent_queries: 20
max_execution_time: 60s
requests_per_minute: 200
max_queue_size: 40
max_queue_time: 20s
clusters:
- name: "analytics"
# Requests are spread round-robin + least-loaded across these nodes.
# Unhealthy nodes are skipped automatically.
nodes:
- "10.0.0.11:8123"
- "10.0.0.12:8123"
- "10.0.0.13:8123"
users:
- name: "default"
password: "clickhouse-secret"Run it:
./chproxy -config=/etc/chproxy/config.ymlNow your application points at chproxy:9090 with the user app / app-secret, and chproxy distributes each query across the three ClickHouse nodes.
The chproxy parameters that matter
| Parameter | What it does |
|---|---|
allowed_networks | Whitelist of subnets allowed to connect. Your first line of defense. |
nodes | The ClickHouse HTTP endpoints to balance across. |
max_concurrent_queries | Hard cap on simultaneous queries per user — protects nodes from overload. |
max_execution_time | Kills runaway queries for that user. |
requests_per_minute | Rate limit per user. |
max_queue_size / max_queue_time | Queue bursts instead of rejecting them. |
to_cluster / to_user | Map an incoming user to a backend cluster and ClickHouse user, hiding real credentials. |
What is HAProxy?
HAProxy is a general-purpose, battle-tested load balancer. It knows nothing about ClickHouse specifically, but it is fast, rock-solid, and can balance both the HTTP interface and the native TCP protocol. The trade-off is that the "smart" ClickHouse-aware features (per-user query limits, queueing, response cache) simply don't exist — HAProxy balances traffic, nothing more.
How well it balances depends entirely on which protocol you point it at:
- HTTP interface (8123) in HTTP mode: HAProxy sees each request individually, so it can do true per-request balancing and use ClickHouse's
/pingendpoint for health checks. This works great. - Native protocol (9000) in TCP mode: HAProxy operates at Layer 4. It picks a backend when the connection is established and pins every query on that connection to the same node. If your client opens one long-lived connection and runs a thousand queries, all thousand hit the same server. This is the number-one gotcha with HAProxy + ClickHouse.
Minimal HAProxy setup
# /etc/haproxy/haproxy.cfg
global
log stdout format raw local0
defaults
log global
timeout connect 5s
timeout client 50s
timeout server 50s
retries 3
# --- HTTP interface (per-request balancing) ---
frontend clickhouse_http
bind *:8123
mode http
default_backend ch_http_nodes
backend ch_http_nodes
mode http
balance roundrobin
option httpchk GET /ping
http-check expect status 200
server ch1 10.0.0.11:8123 check inter 3s fall 3 rise 2
server ch2 10.0.0.12:8123 check inter 3s fall 3 rise 2
server ch3 10.0.0.13:8123 check inter 3s fall 3 rise 2
# --- Native protocol (per-connection balancing) ---
frontend clickhouse_native
bind *:9000
mode tcp
default_backend ch_native_nodes
backend ch_native_nodes
mode tcp
balance leastconn
option tcp-check
server ch1 10.0.0.11:9000 check inter 3s fall 3 rise 2
server ch2 10.0.0.12:9000 check inter 3s fall 3 rise 2
server ch3 10.0.0.13:9000 check inter 3s fall 3 rise 2
Validate and start:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg # "Configuration file is valid"
sudo systemctl restart haproxyThe HAProxy parameters that matter
| Parameter | What it does |
|---|---|
mode http / mode tcp | HTTP for port 8123 (per-request), TCP for port 9000 (per-connection). |
balance roundrobin | Even distribution — good default for the HTTP interface. |
balance leastconn | Sends new connections to the node with fewest active ones — better for the sticky native protocol. |
option httpchk GET /ping | Health-check the HTTP interface using ClickHouse's cheap /ping endpoint. |
option tcp-check | Basic connection-level health check for the native port. |
check inter 3s fall 3 rise 2 | Probe every 3s; mark down after 3 fails, back up after 2 successes. |
On health checks: point them at /ping, which returns Ok. with HTTP 200, requires no auth, and is cheap. Do not use a SELECT 1 query as your health check — it consumes a real connection slot and runs against the engine on every probe.
When is each one advantageous?
Here is the short version:
Use chproxy when:
- Your clients use the HTTP interface (dashboards, Grafana, HTTP drivers,
curl-based ingestion). - You want true per-query load balancing across replicas.
- You need per-user guardrails: concurrency limits, rate limits, execution-time caps, or request queueing to survive bursts.
- You want to hide real ClickHouse credentials behind proxy users.
- You want response caching for repeated dashboard queries.
Use HAProxy when:
- Your clients use the native protocol (
clickhouse-client, native Go/Python drivers) and you just need failover + connection-level spreading. - You need to balance both HTTP and native traffic through one tool.
- You already run HAProxy elsewhere and want one consistent, well-understood LB.
- You need raw throughput and don't need ClickHouse-specific limits.
A very common production pattern is to use both: chproxy for HTTP query traffic (dashboards, reporting apps) where per-query balancing and limits pay off, and HAProxy for native-protocol traffic where you just need TCP failover.
Decision cheat sheet
| Your situation | Best choice |
|---|---|
| Grafana / dashboards over HTTP | chproxy |
| Need per-user query & rate limits | chproxy |
clickhouse-client / native drivers | HAProxy (TCP mode) |
| Balance HTTP and native in one tool | HAProxy |
| Want response caching | chproxy |
| Already standardized on HAProxy | HAProxy (add chproxy later if you need limits) |
Common mistakes to avoid
-
Expecting per-query balancing on the native protocol. HAProxy in TCP mode balances connections, not queries. If your driver holds one connection open, everything lands on one node. Use
leastconn, and if you truly need per-query native balancing, consider client-side balancing in the driver (most native drivers accept multiple endpoints with failover) or route native reads through a distributed setup instead. -
Using
SELECT 1as a health check. Prefer HTTP/ping. It's cheaper and doesn't burn a connection slot. -
Pointing chproxy at port 9000. chproxy is HTTP-only. It must talk to ClickHouse on
8123. -
Leaving
allowed_networksopen. Both proxies can expose ClickHouse to your whole network if you're not careful. Whitelist your app subnets. -
Balancing writes carelessly. Spreading
INSERTs across nodes is fine, but remember ClickHouse batches matter — sending tiny inserts round-robin across many nodes can worsen the "too many parts" problem. Batch first, then balance.
Wrapping up
The choice between HAProxy and chproxy isn't really about which tool is "better" — it's about which protocol your clients speak and what guarantees you need on top of balancing.
If you're on the HTTP interface and want smart, ClickHouse-aware balancing with per-user limits and caching, chproxy is purpose-built for exactly that. If you need to balance the native TCP protocol, or want one general-purpose LB for everything, HAProxy is the reliable workhorse — just respect its per-connection behavior on port 9000.
Start with the protocol your clients use, pick accordingly, and don't be surprised if a mature setup ends up running both.



