Intermittent 502/504 OAuth2 token errors behind HAProxy in Kubernetes

Intermittent 502/504 OAuth2 Token Errors Behind HAProxy in Kubernetes

Problem Description

In a production GKE data‑pipeline, several microservices acquire an OAuth2 access token from an internal /token endpoint. The endpoint is exposed through a HAProxy Ingress controller. Under normal load the token exchange succeeds, but during rolling updates, high‑load spikes, or after pod eviction the following symptoms appear:

  • HTTP 502 Bad Gateway or HTTP 504 Gateway Timeout returned to the client.
  • Client logs show errors such as context deadline exceeded or net/http: request canceled.
  • HAProxy logs contain entries like:
    <...> 0/0/30/120/150 504 0 -- 0/0/0/0/0 0/0
    

    indicating a server timeout after the connect phase.

  • Pipeline jobs abort sporadically, causing data‑loss windows.

These failures are intermittent, affecting only token requests while other API calls continue to work.

Root Cause Analysis

1. HAProxy timeout defaults vs. OAuth2 provider latency

HAProxy’s default timeout server is 50 s (see HAProxy Configuration Manual, “timeout and HTTP keep‑alive settings”). In a multi‑region Kafka connector cluster the OAuth2 provider occasionally needs >50 s to compute a token under load. When the response exceeds timeout server, HAProxy aborts the connection and logs a 504 error.

2. Connection reuse (keep‑alive) and stale backends

HAProxy reuses TCP connections to backend pods by default. If a pod is evicted or not yet ready after a rolling deployment, HAProxy may keep a keep‑alive socket open to the old pod. Subsequent token requests are sent over this stale connection, resulting in “server returned no data or closed connection” (502) as the pod has already terminated. This behavior is described in the community thread “Intermittent 502 on /token endpoint after rolling update”.

3. Health‑check interval too aggressive

The HAProxy Ingress controller’s health checks run every 2 s by default. During a rollout the pod may report UP before the readiness probe passes, so HAProxy continues to route traffic to a pod that is still initializing. The incident “Production data‑pipeline on GKE … health‑checks still reported ‘UP’ while the pod was not ready” illustrates this timing mismatch.

4. Maxconn saturation

A mis‑configured maxconn on the frontend limited concurrent token requests. When the limit was reached, HAProxy returned 502 without contacting the backend, as observed in the CI/CD pipeline case where a specific node hit the limit.

Investigation and Debugging Steps

Log Inspection

# HAProxy log snippet (syslog)
Oct 12 14:23:45 haproxy[1234]: 10.1.2.3:56789 [12/Oct/2026:14:23:45.123] http-in token_backend/token~ token_server/10.244.1.7:8080 0/0/30/120/150 504 0 -- 0/0/0/0/0 0/0

Fields of interest:

  • 0/0/30/120/150 – Tq/Tw/Tc/Tr/Tt (queue, wait, connect, response, total). The 150 ms total is far below the observed latency, indicating HAProxy timed out before the backend responded.
  • 504 – Server timeout.

Tracing the request path

# Capture the token request from a pod
kubectl exec -it pod/data-consumer-abc -- curl -v -X POST http://haproxy-ingress/token \
  -d 'grant_type=client_credentials&client_id=svc&client_secret=***'

Typical output when the failure occurs:

*   Trying 10.96.0.10:80...
* Connected to haproxy-ingress (10.96.0.10) port 80 (#0)
> POST /token HTTP/1.1
> Host: haproxy-ingress
> Content-Length: 58
> Content-Type: application/x-www-form-urlencoded
>
* Empty reply from server
* Connection #0 to host haproxy-ingress left intact
curl: (52) Empty reply from server

Backend readiness verification

# Check pod readiness
kubectl get pod -l app=oauth2-token -o wide
kubectl describe pod oauth2-token-5d9f6c7b8f-xyz
# Look for Ready condition and recent restarts

Health‑check behavior

# HAProxy health‑check logs (if enabled)
Oct 12 14:22:30 haproxy[1234]: 10.1.2.3:56789 [12/Oct/2026:14:22:30.001] http-in token_backend/token~ token_server/10.244.1.7:8080 0/0/0/0/0 200 0 -- 0/0/0/0/0 0/0

If the health‑check reports 200 while the pod is still initializing, the check is too permissive.

Solution

1. Align HAProxy timeouts with OAuth2 service latency

Increase timeout server and timeout http-request to accommodate worst‑case token generation time.

# Before (haproxy.cfg)
defaults
    timeout connect 5s
    timeout client  30s
    timeout server  50s
# After
defaults
    timeout connect 5s
    timeout client  60s
    timeout server  120s   # double the previous value
    timeout http-request 30s

2. Disable keep‑alive for the token endpoint

Force HAProxy to close the connection after each token request, preventing reuse of stale sockets.

# Before
frontend http-in
    bind *:80
    default_backend token_backend

backend token_backend
    server token_server token-service:8080 check
# After (add http-request set-header and option httpclose)
backend token_backend
    option httpclose
    http-request set-header Connection close
    server token_server token-service:8080 check

The option httpclose directive is recommended in the HAProxy “option httpchk” documentation for OAuth2 token endpoints.

3. Harden health‑check timing

Configure a more conservative health‑check interval and use the http-check expect status 200 pattern that also validates the token endpoint’s readiness.

# After (HAProxy Ingress annotations)
haproxy.org/check-interval: "10s"
haproxy.org/check-http: "/token"
haproxy.org/check-http-method: "POST"
haproxy.org/check-http-send: "grant_type=client_credentials&client_id=health&client_secret=health"
haproxy.org/check-http-expect: "status 200"

4. Increase maxconn for the token backend

Set a higher per‑server maxconn to avoid 502 due to connection limits.

# After
backend token_backend
    server token_server token-service:8080 check maxconn 2000

5. Deploy rolling update safety net

Use a pre‑stop hook that drains connections from HAProxy before the pod terminates:

# pod spec snippet
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "curl -X POST http://haproxy-ingress/drain?backend=token_backend"]

This ensures HAProxy stops sending new requests to a pod that is about to disappear.

Verification

Functional test

# Simulate token request after changes
for i in {1..20}; do
  curl -s -o /dev/null -w "%{http_code}\n" http://haproxy-ingress/token \
    -d 'grant_type=client_credentials&client_id=svc&client_secret=***' &
done
wait

Expected output: a stream of 200 status codes with no 502/504.

Metrics validation

  • HAProxy frontend.http_in.responses.2xx should increase.
  • Metrics haproxy_backend_token_backend_server_errors_total for 502/504 should drop to zero.
  • Latency histogram for /token should stay below the new timeout server (120 s).

Log sanity check

# Sample log after fix (no timeout entries)
Oct 12 15:01:10 haproxy[1234]: 10.1.2.3:56789 [12/Oct/2026:15:01:10.321] http-in token_backend/token~ token_server/10.244.1.9:8080 0/0/5/30/35 200 0 -- 0/0/0/0/0 0/0

Prevention and Best Practices

  • Timeout alignment: Match HAProxy timeout server to the 95th‑percentile response time of the OAuth2 service.
  • Connection hygiene: Disable keep‑alive for short‑lived, state‑changing endpoints (e.g., token acquisition).
  • Health‑check fidelity: Use POST health checks that exercise the same code path as production token requests.
  • Graceful draining: Integrate pre‑stop hooks or HAProxy drain API calls during rolling updates.
  • Capacity planning: Monitor maxconn usage and set headroom for bursty token refresh traffic.
  • Observability: Export HAProxy error counters to Prometheus and alert on any rise in 502/504 for the token backend.

Related Topic Hub: Distributed Systems Troubleshooting Hub

FAQ

  1. Why does the token endpoint return 502 only from certain nodes?
    Because those nodes kept a keep‑alive connection to a pod that had been evicted. Disabling keep‑alive forces a new TCP handshake per request, eliminating the stale‑socket scenario.
  2. Can increasing timeout server alone solve the 504 errors?
    It resolves timeouts caused by long token generation, but does not address stale connections or health‑check timing mismatches. All three mitigations are required for a robust fix.
  3. Is it safe to set option httpclose globally?
    For high‑throughput, stateless APIs it may add overhead, but for short‑lived token exchanges the performance impact is negligible and the reliability gain outweighs the cost.
  4. How do I verify that HAProxy is no longer reusing connections?
    Enable option httplog and inspect the Connection header in the logs. Each request should show Connection: close and a new Server: : pair.
  5. What alert thresholds should I set for token‑related 502/504 errors?
    Alert if the rate of 502/504 for the /token path exceeds 1 % of total token requests over a 5‑minute window, or if the latency exceeds 80 % of the configured timeout server.