APISIX Gateway 504s After Exactly 15 Seconds: Tracing It to tcp_syn_retries
Our production API gateway (API7 Enterprise, a commercial distribution of Apache APISIX) started returning intermittent 504s. Every single one of those requests took exactly 15 seconds. Not 14, not 16 — 15.
The plugin timeout was configured at 600 seconds, so the application layer had no reason to give up that fast. Where does 15 seconds come from? This post walks through how we narrowed it down to tcp_syn_retries=3 and a SYN retransmission timeout — and why we deliberately did not ship the obvious fix.
Following the timeout through the code
First step: verify that the 600-second timeout actually reaches the bottom of the stack. Tracing the call chain from the plugin configuration, through the Lua layer, through the FFI boundary, into the C module:
1 | -- ai-transport/http.lua:145 |
The 600s value is passed down correctly, and the C module really does arm a 600s timer. So the timeout is not coming from the application. Something else owns the 15-second deadline.
Suspecting the kernel
15 seconds is a suspicious number. It smells like a kernel TCP constant rather than an application default. Checking the kernel parameters on the node running the gateway pod:
1 | sysctl net.ipv4.tcp_syn_retries |
The kernel computes the total SYN timeout as:
1 | timeout = ((2 << boundary) - 1) * rto_base |
With tcp_syn_retries=3 and a 1-second base RTO:
1 | timeout = ((2 << 3) - 1) * 1 = 15 seconds |
An exact match. The retransmission timeline:
1 | 1st SYN: sent t=0s, times out t=1s (cumulative 1s) |
There is no fifth attempt. tcp_syn_retries=3 means at most 3 retransmissions, 4 attempts total, and then the kernel declares the connection dead.
Eliminating DNS
Could this be DNS resolution failing slowly? Looking at APISIX’s DNS resolution code for the error message formats:
1 | -- resolver.lua:83 |
Searching the gateway logs for these strings did turn up DNS errors:
1 | 2026/09/07 08:59:15 [warn] base.lua:255: failed to send request to LLM server: |
But when we pulled the request_ids of those DNS errors from our log store and compared the two populations, a clear difference emerged:
| DNS errors | SYN timeouts | |
|---|---|---|
| count | 21 | 750 |
| avg duration | 2.25s | 15.38s |
| status code | 500 | 504 |
DNS failures return immediately — they never sit for 15 seconds. Two independent problems that merely look similar.
Packet capture
Theory is nice; packets are better. Capturing traffic on the gateway node and filtering for handshakes:
1 | # find SYN packets |
We captured a complete retransmission sequence from a single connection:
1 | 10.200.76.55.18494 -> 39.96.198.249.443: |
Distribution of the gaps across all captured failures:
| gap | count | theory |
|---|---|---|
| 1.0s | 2 | ✓ |
| 3.0s | 2 | ✓ |
| 7.1s | 2 | ✓ |
The wire-level evidence matches the kernel parameter calculation exactly.
Three layers of evidence
| layer | evidence | conclusion |
|---|---|---|
| application | 717 requests with request_time = 15s | timeout is pinned at exactly 15s, not at 600s |
| code | source trace: 600s propagates correctly | not an application timeout |
| network | PCAP: SYN retransmit gaps of 1s, 3s, 7s | kernel SYN retransmission timeout |
All three layers point to the same root cause: tcp_syn_retries=3 combined with SYNs that never got answered.
The fix we did not ship
The tempting fix — bumping tcp_syn_retries to 5 — was rejected on purpose.
It treats the symptom, not the disease:
- The root cause stays. Why are the SYNs not getting through, or not getting answers? Something at the network level is broken. Retrying more often does not repair it.
- Failure just takes longer.
tcp_syn_retries=5stretches the total timeout from 15s to 63s. Users now wait over a minute to learn the same thing. That is a worse experience, not a better one. - It hides the real problem. A longer timeout makes the symptom recede from dashboards and alerts while the underlying connectivity issue keeps getting worse.
The correct directions: fix connectivity at the network layer, or implement retry/fallback in the application layer where you control backoff, budgets and error semantics — rather than depending on kernel-level retries you cannot observe or tune per-request.
We followed up by pulling in the corporate network team for a deeper investigation of the network layer. That is where the actual fix lives.
Takeaways
- Odd, round timeout values are a fingerprint. 15s, 31s, 63s are almost never application configuration. They are the kernel telling you its retry budget expired.
tcp_syn_retries=Nyields total timeouts of 1s, 3s, 7s, 15s, 31s, 63s for N = 0…5. - Cross-validate across layers. Logs alone suggested DNS. Source code alone exonerated the application. Packet capture alone showed retransmissions but not their business impact. Only the combination — request-duration histograms from logs, source-level timeout tracing, and PCAP — pinned the root cause.
- Similar-looking errors can be unrelated. The DNS failures and the SYN timeouts appeared in the same gateway, in the same period, but had completely different duration profiles. Always compare distributions, not anecdotes.
- A retry knob is not a fix. If you cannot explain why packets are being dropped, more retries just convert a fast failure into a slow one.
The gateway in this incident is API7 Enterprise, a commercial distribution of Apache APISIX. The fix discussed above was deliberately not applied; connectivity was escalated to the network team instead.
This article is the English edition of a post originally published in Chinese.