Building a Custom Query Router with HAProxy
Deploying a stateless query router at the network edge is a proven strategy for achieving zero-downtime horizontal scaling across partitioned database clusters. HAProxy operates at the TCP level and excels at connection routing, health checking, and failover. However, it cannot parse SQL statement content by default — routing decisions based on query type or shard keys require a SQL-aware proxy (ProxySQL, PgBouncer) or a Lua script for payload inspection. This guide provides a production-ready implementation, focusing on precise TCP routing, health checks, and automated failover.
Architecture & Routing Topology Design
Before deploying the proxy layer, establish a deterministic mapping between application query patterns and HAProxy frontend/backend structures. Routing decisions must align with explicit partition boundaries — typically tenant_id, geographic region, or date ranges — to prevent cross-shard latency and data consistency violations.
When designing your topology, align HAProxy ACL priorities with established Cross-Partition Querying & Aggregation Strategies to ensure multi-shard operations are handled predictably. The architecture should isolate dedicated backend pools for:
- Write-Primary Nodes: Handles
INSERT,UPDATE, andDELETEoperations routed to the authoritative shard. - Read-Replica Nodes: Distributes
SELECTtraffic across scaled-out replicas to offload primary I/O. - Cross-Shard Aggregator Nodes: Acts as a fallback for queries requiring federated joins or global aggregations.
Stateless proxies evaluate connections independently. Without session persistence or transaction-aware routing, subsequent queries in a multi-statement transaction may hit different backends unless sticky routing is configured. Map routing rules directly to your Application-Level Sharding Logic to maintain transaction integrity.
HAProxy Configuration Syntax & ACL Setup
HAProxy in TCP mode routes database connections without parsing SQL. To route by query type, you need tcp-request content inspection with a Lua script, or a pre-negotiated protocol-level marker. The example below shows standard TCP routing with tenant-based ACLs using req.ssl_sni (for TLS) or connection source as routing signals:
global
maxconn 50000
defaults
mode tcp
timeout connect 5s
timeout client 30s
timeout server 30s
option tcplog
frontend db_router
bind *:5432
mode tcp
option tcplog
# Use source IP ranges to route tenant clusters to specific backends
acl tenant_a_network src 10.1.0.0/24
acl tenant_b_network src 10.2.0.0/24
use_backend shard_a if tenant_a_network
use_backend shard_b if tenant_b_network
default_backend aggregator_node
For SQL-content-based routing (e.g., distinguishing SELECT from INSERT), use tcp-request inspect-delay with a Lua script to parse the first bytes of the MySQL/PostgreSQL protocol. This approach has performance implications and requires careful regex anchoring to avoid partial-packet mismatches.
# SQL-content inspection via payload (requires protocol awareness)
frontend db_router_sql
bind *:3306
mode tcp
option tcplog
tcp-request inspect-delay 5s
tcp-request content accept if { req.payload(0,10) -m found }
# Anchored regex on first 50 bytes to identify write vs. read
acl is_write req.payload(0,50) -m reg -i ^(INSERT|UPDATE|DELETE|BEGIN)
acl is_read req.payload(0,50) -m reg -i ^SELECT
use_backend shard_primary if is_write
use_backend shard_replica if is_read
default_backend shard_primary
Critical limitation: This payload inspection only works on the first query of a new TCP connection. Connection poolers reuse connections across queries, so HAProxy will never see subsequent query bytes. For per-query routing, use ProxySQL (MySQL) or a middleware layer.
Health Checks & Failover Routing
Zero-downtime execution depends on accurate node readiness validation and automatic partition failover. HAProxy supports database-specific health probes:
backend shard_a
mode tcp
option mysql-check user haproxy_check
server db_a1 10.0.1.10:3306 check inter 5s fall 3 rise 2
server db_a2 10.0.1.11:3306 check backup
backend shard_b
mode tcp
option mysql-check user haproxy_check
server db_b1 10.0.2.10:3306 check inter 5s fall 3 rise 2
server db_b2 10.0.2.11:3306 check backup
backend aggregator_node
mode tcp
server agg1 10.0.3.10:3306 check inter 10s fall 2 rise 3
Use option pgsql-check for PostgreSQL backends. The fall 3 rise 2 thresholds define how many consecutive failed/successful health checks trigger failover or recovery. The backup directive marks a server as standby; HAProxy only routes to it when all non-backup servers in the pool are down.
Performance Optimization & Connection Management
High-throughput workloads require precise tuning of HAProxy’s connection lifecycle. Key settings:
maxconnat both global and backend levels: Set to match your database’smax_connectionsminus reserved slots for superuser and replication.timeout clientandtimeout server: Match your longest expected query duration plus network RTT. Long-running aggregations may need 30–60s.stick-table: For read-your-writes consistency, bind a client session to a specific backend for its duration:
backend shard_primary
mode tcp
stick-table type ip size 100k expire 30s
stick on src
server db_primary 10.0.1.10:5432 check inter 5s fall 3 rise 2
For distributed deployments, coordinate routing weights with cross-datacenter topology. Route local application traffic to geographically proximate database partitions first, using HAProxy’s weight directive to prefer low-latency paths while preserving global failover capability.
Failure Mode Analysis & Operational Mitigation
| Failure Mode | Root Cause | Mitigation Strategy |
|---|---|---|
| High CPU Overhead on Payload Inspection | Complex or greedy regex on req.payload forces HAProxy to buffer and scan packets |
Use anchored, literal prefix matching (-m beg or -m str) restricted to the first 50 bytes where SQL verbs reside |
| Split Routing Mid-Transaction | HAProxy routes per-connection by default; connection poolers reuse connections | Use stick-table to bind sessions to a backend, or route at the application layer with explicit transaction affinity |
| Overlapping ACL Priorities | Broad is_read rules above specific tenant rules route all reads to the wrong backend |
Order use_backend directives from most specific to least specific |
FAQ
Can HAProxy route based on SQL query content?
Only the first query on a new TCP connection, using tcp-request inspect-delay and req.payload ACLs. Connection-pooled environments will not benefit from per-query routing at the HAProxy level; use ProxySQL or a SQL-aware middleware layer.
How does HAProxy handle cross-shard joins? HAProxy cannot execute joins. It routes connections to a designated aggregator node or requires application-level query decomposition to fetch and merge results client-side.
Does HAProxy support database connection pooling? No, HAProxy routes raw TCP streams. Pair it with PgBouncer (PostgreSQL) or ProxySQL (MySQL) for true connection pooling, query caching, and advanced transaction management.