HAProxy timeouts during batch ingestion to multi-GPU training cluster

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:

  1. Data is streamed from the client to HAProxy, then from HAProxy to the backend. Each hop incurs TCP retransmission and buffer copy latency.
  2. 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

  1. Functional test: Use curl to 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.

  1. Log inspection: Confirm absence of SC or sH entries for the test request.

grep "SC\|sH" /var/log/haproxy.log

Should return no lines.

  1. Metrics validation: HAProxy’s show stat should show qcur (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 server expiring while the node was still processing.
  • Assumption pitfall: Assuming “large payload” only affects tune.bufsize ignored 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 roundrobin still routed new uploads to it, causing queue buildup. Adding option httpchk with 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 client and timeout server to a multiple of the worst‑case batch processing time plus a safety margin.
  • Increase tune.bufsize when payloads exceed 1 MB to avoid premature header truncation.
  • Monitor HAProxy’s scur (current sessions) and qcur (queue length) metrics; trigger alerts if qcur > 20 % of maxconn.
  • 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

  1. Why does increasing only timeout client sometimes not fix the problem?
    Because the backend may also close the connection after its own timeout server expires. Both sides must be extended to cover the full processing window.
  2. Can I keep the default tune.bufsize and 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. Raising tune.bufsize prevents this bottleneck.
  3. What is the impact of raising tune.maxaccept on CPU usage?
    Higher maxaccept allows more simultaneous accept() calls, which can increase per‑core context switches. Monitor CPU and softirq usage; if they spike, consider scaling HAProxy horizontally.
  4. 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.
  5. Is timeout tunnel relevant for batch ingestion?
    Only for protocols that switch to TCP tunneling (e.g., WebSocket). For pure HTTP POSTs, timeout client and timeout server govern the entire request‑response cycle.