Problem: HAProxy Timeouts During High‑Throughput Batch Ingestion
In a multi‑GPU training cluster, HAProxy fronts a set of tensorflow/torchserve workers that accept large JSON or binary payloads via HTTP POST. During nightly batch uploads (10 GB + per request) clients observe:
- HTTP 504 “Gateway Timeout” with HAProxy log entry
SC(client timeout) - Intermittent HTTP 502 “Bad Gateway” with log entry
sH(server closed before response) - Connection resets (
cR) when backend nodes are saturated
Symptoms manifest only under sustained high concurrency (hundreds of parallel uploads) and disappear when the load is throttled.
Root Cause Analysis
Timeout defaults are too aggressive for long‑running uploads
HAProxy’s default timeout client is 50 s (see HAProxy Configuration Manual – timeout settings). Batch ingestion to GPU nodes frequently exceeds this window because:
- Data is streamed from the client to HAProxy, then from HAProxy to the backend. Each hop incurs TCP retransmission and buffer copy latency.
- GPU workers perform preprocessing (e.g., sharding, TFRecord conversion) that can take minutes for a 10 GB batch.
When the client‑side timeout expires, HAProxy aborts the connection and logs SC, which matches the “504 Gateway Timeout” observed in production at the fintech firm.
Insufficient HTTP request buffer
HAProxy allocates tune.bufsize (default 16 KB) for request headers and the first part of the body. Large JSON batches trigger the “client timeout” before the full body is read, as documented in the HAProxy Performance Tuning Guide – tune.bufsize. GitHub issue #2459 and the Stack Overflow answer (76184231) both point out that increasing timeout http-request and the request buffer resolves similar failures.
Connection‑accept throttling (tune.maxaccept)
During peak ingestion, HAProxy’s tune.maxaccept limit (default 100) can cause request queuing. The edge‑AI incident cited a “connection reset by peer” (cR) caused by this queuing, which then propagates as a client‑side timeout.
Investigation and Debugging Steps
1. Collect HAProxy logs with full timestamps
# /etc/haproxy/haproxy.cfg
global
log /dev/log local0 info
log-tag haproxy
defaults
log global
mode http
option httplog
timeout client 50s
timeout server 50s
timeout connect 5s
Sample log entry when the timeout fires:
Aug 12 02:15:43 haproxy[12345]: CLIENT_IP:PORT [12/Aug/2026:02:15:43.123] http-in~ backend/server1 0/0/52/120/200 504 0 - - SC 5/5/0/0/0 0/0 "POST /ingest HTTP/1.1"
2. Use the Runtime API to inspect stuck sessions
echo "show sess" | socat /var/run/haproxy.sock stdio | grep -i timeout
Typical output shows sessions in SC state with tq (time in queue) and tw (time waiting for backend) counters.
3. Capture TCP flow to confirm payload size and retransmission spikes
tcpdump -i eth0 -s 0 -w ingest.pcap host CLIENT_IP and port 80
Look for repeated retrans flags and long ACK delays that indicate buffer exhaustion.
4. Verify backend processing time
# On a GPU node
journalctl -u training-service -f | grep "batch_id"
Typical lines show processing durations of 180‑300 s for a 10 GB batch, far exceeding the 50 s client timeout.
Solution: Tuning HAProxy for Large, Long‑Running Batch Uploads
Configuration Changes
| Parameter | Default | Recommended Value | Reason |
|---|---|---|---|
timeout client |
50s | 600s | Accommodates multi‑minute uploads |
timeout server |
50s | 600s | Allows backend processing time |
timeout http-request |
10s (implicit) | 300s | Prevents premature abort while reading large bodies |
tune.bufsize |
16KB | 128KB | Buffers larger request headers and initial payload chunks |
tune.maxaccept |
100 | 1000 | Reduces queuing under bursty ingestion |
maxconn |
2000 | 5000 | Matches GPU node capacity and prevents connection starvation |
Before / After Comparison
# Before (original)
global
tune.bufsize 16384
tune.maxaccept 100
defaults
timeout client 50s
timeout server 50s
timeout connect 5s
maxconn 2000
# After (tuned)
global
tune.bufsize 131072
tune.maxaccept 1000
defaults
timeout client 600s
timeout server 600s
timeout connect 5s
timeout http-request 300s
maxconn 5000
Deploy the updated configuration
haproxy -c -f /etc/haproxy/haproxy.cfg # validate syntax
systemctl reload haproxy # apply without downtime
Verification
- Functional test: Use
curlto POST a 12 GB JSON batch and measure end‑to‑end latency.
time curl -X POST http://haproxy.example.com/ingest \
-H "Content-Type: application/json" \
--data-binary @large_batch.json \
-o /dev/null -w "%{http_code}"
Expected output: 200 and total time < 600 s. No “504” or “502” codes.
- Log inspection: Confirm absence of
SCorsHentries for the test request.
grep "SC\|sH" /var/log/haproxy.log
Should return no lines.
- Metrics validation: HAProxy’s
show statshould showqcur(current queue) near zero even under load.
echo "show stat" | socat /var/run/haproxy.sock stdio | grep "^frontend,http-in"
Operational Experience and Lessons Learned
- Misleading symptom: Initial 502 errors were blamed on GPU node crashes, but deeper log analysis revealed they were downstream effects of HAProxy’s
timeout serverexpiring while the node was still processing. - Assumption pitfall: Assuming “large payload” only affects
tune.bufsizeignored the fact that HAProxy treats the entire upload as part of the client‑side timeout. - Production edge case: When a single backend node became overloaded, HAProxy’s
balance roundrobinstill routed new uploads to it, causing queue buildup. Addingoption httpchkwith a lightweight health‑check endpoint allowed HAProxy to temporarily skip the saturated node. - Lesson: Always align HAProxy’s timeout horizon with the longest expected request‑processing time in the data path, especially for AI workloads that involve heavy preprocessing.
Best Practices and Prevention
- Set
timeout clientandtimeout serverto a multiple of the worst‑case batch processing time plus a safety margin. - Increase
tune.bufsizewhen payloads exceed 1 MB to avoid premature header truncation. - Monitor HAProxy’s
scur(current sessions) andqcur(queue length) metrics; trigger alerts ifqcur> 20 % ofmaxconn. - Implement health‑checks that verify backend readiness for large uploads (e.g.,
/ready?size=10GB). - Periodically run a synthetic batch upload benchmark to validate timeout settings after any configuration change.
Related Topic Hub: Distributed Systems Troubleshooting Hub
FAQ
- Why does increasing only
timeout clientsometimes not fix the problem?
Because the backend may also close the connection after its owntimeout serverexpires. Both sides must be extended to cover the full processing window. - Can I keep the default
tune.bufsizeand just increase timeouts?
No. Large POST bodies are partially buffered; if the buffer fills before the timeout fires, HAProxy will pause reading and may trigger a client timeout. Raisingtune.bufsizeprevents this bottleneck. - What is the impact of raising
tune.maxaccepton CPU usage?
Highermaxacceptallows more simultaneous accept() calls, which can increase per‑core context switches. MonitorCPUandsoftirqusage; if they spike, consider scaling HAProxy horizontally. - How do I differentiate between a client‑side timeout (
SC) and a server‑side timeout (SH) in logs?
HAProxy log flags:SC= client timeout,SH= server timeout. The flag appears after the HTTP status code in the log line. - Is
timeout tunnelrelevant for batch ingestion?
Only for protocols that switch to TCP tunneling (e.g., WebSocket). For pure HTTP POSTs,timeout clientandtimeout servergovern the entire request‑response cycle.