Envoy Gateway Slow Upload Timeout: Fix HTTP 408

Tested on RHEL 10.2 (Coughlan)
Package kubectl v1.37.0
Envoy Gateway v1.9.2
ingress-nginx controller v1.15.1
Gateway API CRDs v1.6.1 (standard channel)
Applies to Any host with kubectl configured; any Kubernetes cluster that already runs Envoy Gateway and ingress-nginx
Assumes An existing Gateway and HTTPRoute in front of an application that accepts a POST body
Privilege Normal user
Scope How requestReceivedTimeout limits the time allowed to receive a request, and how that differs from ingress-nginx client-body-timeout and from a 504 response deadline. Does not cover body-size limits or keep-alive idle close.
Related guides Fix an Envoy Gateway 504
Migrate proxy-read-timeout to Envoy Gateway
Replace proxy-body-size on Envoy Gateway
Migrate ingress-nginx to Envoy Gateway
Gateway API with HTTPRoute
Short on time?Beta Get the gist of this 6-minute read in seconds, then ask follow-up questions

A slow upload is not a slow backend:

  • timeouts.request is the time allowed for the response
  • ClientTrafficPolicy timeout.http.requestReceivedTimeout is the time allowed to receive the request

On Envoy Gateway v1.9.2, a 2 second receive limit returned HTTP 408 and the body request timeout for 8000 bytes sent at 1000 bytes per second. The same 8000 bytes, sent without a rate limit, returned HTTP 200 and bytes=8000. The file size did not change. The pace did. A 413 is a size rejection. This 408 is a clock.

client-body-timeout is a gap between reads

bytes --1s-- bytes --1s-- bytes
No 2-second gap, so a slow steady upload can continue

requestReceivedTimeout is the total time to receive the request

start ---------------- 2s
                       receive timer
The whole body is not in yet, so the upload stops

Set requestReceivedTimeout

The field is on a ClientTrafficPolicy, gateway.envoyproxy.io/v1alpha1. This test targeted the Gateway:

  • The timer starts when the request starts and stops when the request has been received
  • It does not replace timeouts.request. Put a request deadline on the route that is long enough for the upload, or the 15 second response clock can fire as well

30s below is only so the test is not also a 504. 2s is the receive limit that produced the 408:

yaml
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
  name: receive-timeout
spec:
  targetRefs:
  - group: gateway.networking.k8s.io
    kind: Gateway
    name: eg
  timeout:
    http:
      requestReceivedTimeout: 2s

In this v1.9.2 test, that limit produced HTTP 408 and the body request timeout. The v1.9 task page describes a different status for the same field. Use the status you observe on the version you run. Do not treat 408 and that body as the result on every release.

The fast POST of 8000 bytes returned HTTP 200. Send it with no rate limit:

bash
curl -sS -D - --max-time 20 -o upload.txt -H 'Host: app.example.com' \
  --data-binary @upload.bin 'http://127.0.0.1:19080/upload'
output
HTTP/1.1 200 OK
server: BaseHTTP/0.6 Python/3.12.15
date: Sat, 03 Oct 2026 19:20:39 GMT
content-type: text/plain
content-length: 11

The same bytes at 1000 bytes per second need about 8 seconds. Send that rate-limited POST. The 408 is timestamped 19:20:41, about 2 seconds later:

bash
curl -sS -D - --max-time 20 -o upload.txt -H 'Host: app.example.com' \
  --limit-rate 1000 --data-binary @upload.bin 'http://127.0.0.1:19080/upload'
output
HTTP/1.1 408 Request Timeout
content-length: 15
content-type: text/plain
date: Sat, 03 Oct 2026 19:20:41 GMT
connection: close

The body is request timeout:

  • There is no application server line
  • The application did not reject the body
  • The receive clock closed the upload

These captures used a local address. Send the same Host and body to the hostname your Gateway publishes.


client-body-timeout is a gap, not the whole upload

ingress-nginx client-body-timeout is a different rule:

  • It is the gap allowed between reads of the client body
  • It is a controller ConfigMap setting, not an Ingress annotation
  • The default is 60 seconds

Set to 2 seconds, the same 8000 bytes at 1000 bytes per second still returned HTTP 200 and bytes=8000. The client waited 8.005 seconds. Bytes kept arriving, so the 2 second gap never fired.

nginx client-body-timeout: 2s

bytes --1s-- bytes --1s-- bytes
        ok          ok
No 2-second gap, so the upload is allowed

Envoy requestReceivedTimeout: 2s

start ---------------- 2s
                       stop
The whole body is not received yet

A stall is different from a trickle:

  • With client-body-timeout at 2 seconds, a body that sent some bytes and then stopped was closed at 3.03 seconds with an empty read and no HTTP status line
  • Envoy's 408 was the slow but continuous upload
  • nginx's close was the pause

Do not copy a client-body-timeout number into requestReceivedTimeout and expect the same trickle to pass.


Troubleshooting

Symptom Likely cause What to change
HTTP 408 and request timeout while the body is still uploading, on v1.9.2 requestReceivedTimeout is shorter than the upload Raise it, or send the body faster. Confirm the status on your version
The same file returns 200 when it is sent quickly The failure is pace, not size Do not raise a body-size limit for this 408
HTTP 504 and upstream request timeout The response deadline, often the 15 second default Set timeouts.request. That does not extend a receive timeout
A steady slow upload works on ingress-nginx and fails on Envoy client-body-timeout is a gap. requestReceivedTimeout is the total receive time Do not copy the nginx number across as if it were the same timer
nginx closes a paused upload with no status line client-body-timeout saw a gap with no bytes That is the stall. It is not the Envoy 408 from a continuous slow send

Summary

On Envoy Gateway v1.9.2, requestReceivedTimeout: 2s returned HTTP 408 and request timeout for 8000 bytes sent at 1000 bytes per second. The same 8000 bytes, sent without a rate limit, returned HTTP 200 and bytes=8000. The clock is how long receiving the request may take, not how large the body is.

ingress-nginx client-body-timeout: 2 still allowed that steady upload, which finished in 8.005 seconds, because the gap between reads never reached 2 seconds. A body that stopped sending was closed with no status line. Those are not the same failure as the Envoy 408, and neither one is the 504 from a slow response.


References


Frequently Asked Questions

Why does a slow upload return 408 when a fast upload of the same file returns 200?

requestReceivedTimeout limits how long the proxy waits to receive the request. On v1.9.2, 2 seconds was enough for 8000 bytes sent at full speed and not enough for the same bytes at 1000 bytes per second. The fast call returned 200 and bytes=8000. The slow call returned 408 and request timeout.

Is this the same as a 413?

No. A 413 means the body was rejected for size. This 408 happened to a body that succeeded when it was sent faster. The size did not change.

Is client-body-timeout the same number?

No. It is the gap between reads of the client body. At 2 seconds, a steady 8 second upload still returned 200. requestReceivedTimeout at 2 seconds stopped that same steady upload.

Will every Envoy Gateway version return HTTP 408 for this field?

Use the status on the version you run. This v1.9.2 test returned HTTP 408 and request timeout. The v1.9 task page describes a different status for the same setting.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)