All posts
HAProxy vs CHproxy for ClickHouse: Which Load Balancer Should You Use?

HAProxy vs CHproxy for ClickHouse: Which Load Balancer Should You Use?

September 7, 20269 min readCan Sayin
Share on social:

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 by curl, the JDBC/ODBC HTTP drivers, most language clients in HTTP mode, and dashboards like Grafana.
  • Native TCP protocol — port 9000. Used by clickhouse-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.yml

Now 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

ParameterWhat it does
allowed_networksWhitelist of subnets allowed to connect. Your first line of defense.
nodesThe ClickHouse HTTP endpoints to balance across.
max_concurrent_queriesHard cap on simultaneous queries per user — protects nodes from overload.
max_execution_timeKills runaway queries for that user.
requests_per_minuteRate limit per user.
max_queue_size / max_queue_timeQueue bursts instead of rejecting them.
to_cluster / to_userMap 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 /ping endpoint 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 haproxy

The HAProxy parameters that matter

ParameterWhat it does
mode http / mode tcpHTTP for port 8123 (per-request), TCP for port 9000 (per-connection).
balance roundrobinEven distribution — good default for the HTTP interface.
balance leastconnSends new connections to the node with fewest active ones — better for the sticky native protocol.
option httpchk GET /pingHealth-check the HTTP interface using ClickHouse's cheap /ping endpoint.
option tcp-checkBasic connection-level health check for the native port.
check inter 3s fall 3 rise 2Probe 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 situationBest choice
Grafana / dashboards over HTTPchproxy
Need per-user query & rate limitschproxy
clickhouse-client / native driversHAProxy (TCP mode)
Balance HTTP and native in one toolHAProxy
Want response cachingchproxy
Already standardized on HAProxyHAProxy (add chproxy later if you need limits)

Common mistakes to avoid

  1. 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.

  2. Using SELECT 1 as a health check. Prefer HTTP /ping. It's cheaper and doesn't burn a connection slot.

  3. Pointing chproxy at port 9000. chproxy is HTTP-only. It must talk to ClickHouse on 8123.

  4. Leaving allowed_networks open. Both proxies can expose ClickHouse to your whole network if you're not careful. Whitelist your app subnets.

  5. 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.

Need help with your data platform?

BlancoByte designs and runs real-time pipelines and modern data infrastructure. We work alongside your team, from architecture to production.

Share on social: