| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | kubectl v1.37.0Envoy Gateway v1.9.2ingress-nginx controller v1.15.1Gateway API CRDs v1.6.1 (standard channel) |
| Applies to | Any host with kubectl configured; any Kubernetes cluster that already runs ingress-nginx and Envoy Gateway |
| Assumes | An existing ingress-nginx Ingress and an Envoy Gateway HTTPRoute for the same application |
| Privilege | Normal user |
| Scope | How proxy-read-timeout behaves, why streamIdleTimeout is the closer inactivity control, and why timeouts.request is an absolute deadline. Does not cover slow uploads, idle keep-alive connections, or WebSocket stream tuning. |
| Related guides | Fix an Envoy Gateway 504 Fix a slow Envoy Gateway upload Migrate ingress-nginx to Envoy Gateway Gateway API with HTTPRoute Expose services with Ingress |
nginx.ingress.kubernetes.io/proxy-read-timeout does not become timeouts.request on an HTTPRoute. One is the gap allowed between reads of the upstream response. The other is a deadline for the whole request. Copying 60 onto timeouts.request: 60s can cut a long download or a streamed response that nginx was still willing to carry, as long as the backend kept writing.
Do not translate the number until you know which behavior you need:
- A backend that stays silent until the end makes the two look the same
- A backend that keeps sending data does not
proxy-read-timeout is a gap between upstream reads
data ----10s---- data ----10s---- data
No 60-second silent gap, so the response can continue
HTTPRoute timeouts.request is an absolute deadline
start ------------------------------ 60s
deadlineWhat replaces proxy-read-timeout?
| ingress-nginx | Envoy Gateway | Same behavior? |
|---|---|---|
proxy-read-timeout |
streamIdleTimeout, the quiet period on the stream |
Closest inactivity match |
proxy-connect-timeout |
Backend TCP connect timeout | Closest match |
proxy-send-timeout |
No single annotation equivalent | Depends on the request |
| No absolute request deadline | HTTPRoute timeouts.request |
Different. This is a total deadline |
| Per-try upstream timeout | timeouts.backendRequest |
Different scope. It cannot exceed request |
client-body-timeout |
requestReceivedTimeout |
Related, and not the same. That is the slow upload case |
streamIdleTimeout and timeouts.request are both still in play:
streamIdleTimeoutis how long a stream may sit with no upstream or downstream activity. That is the same idea as a gap between reads. On Envoy Gateway v1.9 it isBackendTrafficPolicytimeout.http.streamIdleTimeout, not a field on the HTTPRoute.timeouts.requestis a separate deadline. Envoy Gateway's default is about 15 seconds. Raising only the idle gap does not remove it. A response that produces nothing for 16 seconds still hits the request clock. The 15 second 504 page is that failure.
How proxy-read-timeout behaves
The annotation is a whole number of seconds:
- It does not take an
ssuffix - The controller default is 60
The Ingress below is that setting, shown at 60 seconds:
metadata:
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"ingress-nginx turns that into proxy_read_timeout. Two details matter when you read a failure:
- The timer resets when the upstream sends more of the response. Activity keeps the request alive past 60 seconds.
- The timeout applies to each upstream read attempt. Retries can make the client wait longer than the annotation. On this controller,
proxy-read-timeout: "3"with the default of 3 tries returned 504 after 9.021 seconds. Settingproxy-next-upstream-triesto1made the same sleep return the nginx HTML 504 in 3.025 seconds.
Do not treat the annotation as the total time the client will always wait.
A 16 second sleep, with the default 60 second read timeout and no bytes in between, still returned HTTP 200 and slept=16.0. The gap never reached 60 seconds, so nginx waited for the app.
Set the inactivity timeout on Envoy Gateway
For a stream that should survive as long as data keeps moving, set streamIdleTimeout to that gap. 60s below is the same gap as proxy-read-timeout: "60", not a new total deadline:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: stream-idle
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: web
timeout:
http:
streamIdleTimeout: 60sReplace 60s with the quiet period you actually want. This policy does not replace timeouts.request:
- If the route has no request timeout, Envoy Gateway v1.9.2 still stops a silent response at about 15 seconds with HTTP 504 and the body
upstream request timeout - Set the request deadline as well when the whole call must be allowed to run longer
The other two annotations are not this gap:
proxy-connect-timeoutis the time allowed to open the upstream connection. The default on this controller is 5 seconds. Match it with the backend connect timeout if new connections are what you were limiting.proxy-send-timeoutis the gap between writes of the request to the upstream. There is no one field that means that for every workload.
Troubleshooting
| Symptom | Likely cause | What to change |
|---|---|---|
| A long download dies at about 15 seconds after the move | timeouts.request is still the default absolute deadline |
Raise timeouts.request if the whole call must run that long. streamIdleTimeout alone does not remove the 15 second clock |
A streamed response that used to work now dies at exactly timeouts.request |
The number from proxy-read-timeout was copied onto the absolute deadline |
Put the gap on streamIdleTimeout. Use timeouts.request only for the total call |
nginx waited about three times proxy-read-timeout |
The read timeout was retried. Here, 3 tries turned 3 seconds into 9.021 seconds | Check proxy-next-upstream-tries before you copy the client-visible wait |
| The failure is HTTP 408 while the body is still uploading | That is the receive clock, not the upstream read gap | Use the slow-upload page, not proxy-read-timeout |
Summary
proxy-read-timeout is the gap between upstream reads. The closest Envoy Gateway control is streamIdleTimeout. timeouts.request is a deadline for the whole request, and the v1.9.2 default is about 15 seconds.
A 16 second silent response still succeeded through ingress-nginx at the default 60 second read timeout. The same shape of wait is what Envoy cuts with HTTP 504 when the request deadline is left at 15 seconds. Set both clocks when a long, active response must survive the move. Do not copy the annotation onto timeouts.request and call it the same setting.
References
- ingress-nginx custom timeouts
- Envoy Gateway HTTP timeouts (v1.9)
- Envoy Gateway BackendTrafficPolicy (v1.9)
- Gateway API HTTPRoute timeouts
Frequently Asked Questions
Does proxy-read-timeout 60 become timeouts.request 60s?
No. The annotation is a gap between upstream reads. timeouts.request is a deadline for the whole request. A response that keeps sending data can run longer than proxy-read-timeout. The same response stops when timeouts.request is reached.
What is the closest Envoy Gateway field?
streamIdleTimeout on a BackendTrafficPolicy. It is the time a stream may have no activity. You still need timeouts.request when the call must be allowed to run past the default request deadline of about 15 seconds.
Why did a 3 second read timeout take about 9 seconds?
The timeout is per upstream attempt. With 3 tries, the client waited 9.021 seconds. One try returned the nginx 504 in 3.025 seconds. The annotation was 3 seconds in both cases.
Why did a 16 second sleep succeed on ingress-nginx?
The default proxy-read-timeout is 60 seconds, and the app wrote nothing until the end. The gap stayed under 60 seconds, so the response was HTTP 200 and slept=16.0.

