ingress-nginx is retired: what replaces proxy-body-size, proxy-read-timeout and the other proxy annotations?
Annotation by annotation: what ingress2gateway emits, the Gateway API field, Envoy Gateway policy and Traefik's ingress-nginx provider, with the defaults that change.
Key points
- Gateway API core has a portable answer for only part of the list:
timeouts.requestfor the timeouts,RequestRedirectforssl-redirect,RequestHeaderModifierforx-forwarded-prefixandupstream-vhost. Body size and buffer annotations have no Gateway API field and land in implementation policy, such as Envoy Gateway'sBackendTrafficPolicyrequestBuffer.limit. - ingress2gateway does not copy timeout values. It takes the largest of
proxy-connect-timeout,proxy-read-timeoutandproxy-send-timeoutand multiplies it by 10, soproxy-read-timeout: "300"becametimeouts.request: 50m0sin testing on v1.2.0. - The routes most likely to break are the ones with no timeout annotation. They ran on ingress-nginx's 60s read timeout, nothing is emitted for them, and Envoy Gateway applies Envoy's 15s route default.
- With the default
standardemitter,proxy-body-sizeis dropped with a warning. With--emitter=envoy-gatewayit becomesrequestBuffer.limit, andclient-body-buffer-sizeon its own becomes the same hard 413 limit, which it never was in nginx. - Traefik v3.7's
kubernetesIngressNGINXprovider reads the annotations in place, includingauth-url,proxy-bufferingand a subset ofconfiguration-snippetdirectives, but its provider-level defaults are not ingress-nginx's:proxyConnectTimeoutis 60 against ingress-nginx's 5.
Nothing replaces them one for one. Gateway API has portable fields for the timeouts (timeouts.request), for ssl-redirect (a RequestRedirect filter) and for x-forwarded-prefix and upstream-vhost (a RequestHeaderModifier filter). It has no field at all for proxy-body-size, proxy-buffer-size or proxy-buffering, so those move into whatever policy resource your chosen implementation defines, and every default underneath them changes. The other route is to keep the annotations and swap the controller: Traefik v3.7's kubernetesIngressNGINX provider reads most of them as they are, with its own defaults.
The table below takes the proxy-behaviour annotations one at a time across four targets: what ingress2gateway emits for them, the portable Gateway API field if there is one, the Envoy Gateway policy field, and the Traefik provider. ingress2gateway behaviour was tested on v1.2.0 (the README entries quoted below are unchanged since v1.0.0); Envoy Gateway fields are from v1.9.2, Gateway API from v1.6.3, Traefik from the v3.7 reference documentation.
The annotation table#
| Annotation (ingress-nginx default) | ingress2gateway emits | Portable Gateway API field | Envoy Gateway v1.9 | Traefik v3.7 kubernetesIngressNGINX |
|---|---|---|---|---|
proxy-body-size (1m) | standard: dropped, with a warning. envoy-gateway: requestBuffer.limit | None | BackendTrafficPolicy spec.requestBuffer.limit | Supported. Provider default proxyBodySize 1048576 bytes |
proxy-connect-timeout (5) | Folded into timeouts.request: largest of the three x 10 | rules[].timeouts.request (Extended) | BackendTrafficPolicy spec.timeout.tcp.connectTimeout, default 10s | Supported. Provider default 60 |
proxy-read-timeout (60) | Same rule | timeouts.request, timeouts.backendRequest | spec.timeout.http.requestTimeout | Supported. Provider default 60 |
proxy-send-timeout (60) | Same rule | Same | Same | Supported. Provider default 60 |
proxy-buffer-size (4k) | "Unsupported annotation" warning | None | No direct equivalent | Supported. Provider default 8192 bytes |
proxy-buffering (off) | "Unsupported annotation" warning | None | No direct equivalent | Supported. Provider default false |
use-forwarded-headers (false) | Nothing: ConfigMap key, not an annotation | None | ClientTrafficPolicy spec.clientIPDetection.xForwardedFor.numTrustedHops | Traefik entryPoint forwardedHeaders.trustedIPs |
ssl-redirect (true with TLS) | HTTP route with RequestRedirect, scheme: https, 308; false omits it | RequestRedirect filter (Core) | The same HTTPRoute | Supported; cannot opt out per route if enabled globally |
x-forwarded-prefix | RequestHeaderModifier setting the header, only with rewrite-target | RequestHeaderModifier (Core) | The same HTTPRoute | Supported |
upstream-vhost | RequestHeaderModifier setting Host | RequestHeaderModifier, or URLRewrite hostname (Extended) | The same HTTPRoute | Supported |
auth-url | "Unsupported annotation" warning | ExternalAuth filter, experimental channel | SecurityPolicy spec.extAuth.http | Supported; "Forward auth behaves differently than NGINX" |
configuration-snippet | "Unsupported annotation" warning | None | No equivalent | A listed subset of directives |
Defaults in the first column are the ingress-nginx ConfigMap values, which is what an Ingress without the annotation actually ran with. Timeouts there are unitless seconds. Note the connect timeout: ingress-nginx ships proxy-connect-timeout: 5, not the 60s that plain nginx's proxy_connect_timeout defaults to.
What "retired" means for a running cluster#
The Kubernetes announcement of November 2025 gave March 2026 as the end of best-effort maintenance, after which "there will be no further releases, no bugfixes, and no updates to resolve any security vulnerabilities that may be discovered". The last controller releases were v1.15.1, v1.14.5 and v1.13.9, all published on 19 March 2026, and the kubernetes/ingress-nginx repository is now archived on GitHub. The same announcement says existing deployments keep working and installation artefacts stay available.
What stops is the fix for the next CVE, which makes this a scheduled migration rather than an outage: the old controller can keep serving while the new routes are tested beside it.
The two answers that both circulate#
Search for this and you will find two claims. One says ingress2gateway converts your annotations. The other says Gateway API has no body size or buffering, so those annotations are lost. Both are true, and the switch between them is a single command line flag.
ingress2gateway reads Ingress objects and writes Gateway API objects through an emitter. The default is standard, which "will only output Gateway API". Because Gateway API has no body size field, the standard emitter cannot express proxy-body-size and reports it:
Failed to apply default.app.metadata.annotations."nginx.ingress.kubernetes.io/proxy-body-size"
from default/app: Most Gateway API implementations have reasonable body size and buffering defaultsRun the same input with --emitter=envoy-gateway and the annotation reappears as an Envoy Gateway resource. An Ingress carrying proxy-body-size: "50m" produced this alongside the HTTPRoute:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: app-app-example-com
spec:
requestBuffer:
limit: 50Mi
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: app-app-example-comThe other emitters are agentgateway, airlock-microgateway, gce and kgateway. Pick the one for the implementation you are moving to, because standard throws away everything only an implementation policy can hold. The README says the tool "is not intended to copy annotations from Ingress to Gateway API", which is the right way to read its output.
Timeouts: the x10 rule, and the routes that get 15 seconds#
The three ingress-nginx timeouts are nginx's proxy_connect_timeout, proxy_read_timeout and proxy_send_timeout. The read and send timeouts are gaps between two successive operations, not a limit on the whole request, a distinction timeout budgets across proxy hops covers in detail. Gateway API's timeouts.request is the opposite kind of timer. The specification says it "is intended to cover as close to the whole request-response transaction as possible", and when it is unset "request timeout behavior is implementation-specific".
ingress2gateway bridges the two with a fixed formula. Its common emitter takes the largest of the three annotation values on the Ingress and multiplies it by a constant, tcpTimeoutMultiplier = 10, which has been in the source since the v1.0.0 tag. Tested on v1.2.0:
| Annotations on the Ingress | Emitted timeouts.request |
|---|---|
proxy-read-timeout: "60" | 10m0s |
proxy-connect-timeout: "5", proxy-read-timeout: "300", proxy-send-timeout: "60" | 50m0s |
| None | Nothing: no timeouts block at all |
Every converted route also carries the warning "i2gw has made a best-effort translation to Gateway API timeouts.request. Please verify that this meets your needs." Take it literally. A 50 minute total deadline is a loose upper bound for a route that once allowed 300 seconds of silence between reads, but it is not the same control: a response that keeps trickling for an hour is now cut where nginx would have let it run. Set timeouts.request to the longest legitimate response time you expect on that route, and use timeouts.backendRequest for the per-attempt limit if your implementation supports retries. The specification requires backendRequest not to exceed request, since one request "may result in more than one call" to the backend.
The connect timeout has no portable field. Envoy Gateway puts it in BackendTrafficPolicy as spec.timeout.tcp.connectTimeout, documented in the API source as "Default: 10 seconds", which is double ingress-nginx's 5. The same spec.timeout.http block holds requestTimeout and a connectionIdleTimeout that defaults to one hour.
Body size and buffers: no portable field, and one surprise#
Gateway API v1.6.3 has no request body size or proxy buffering field on HTTPRoute. The only body size in the HTTPRoute types is inside the experimental external auth filter, where it limits how much body is sent to the authorization server. Every implementation therefore invents its own, and the semantics differ.
In Envoy Gateway v1.9.2, requestBuffer.limit is documented as "the maximum allowed size in bytes for each incoming request buffer. If exceeded, the request will be rejected with HTTP 413 Content Too Large." That matches what proxy-body-size did in ingress-nginx, which returned a 413 when "the size in a request exceeds the maximum allowed size of the client request body". The mechanism is different, though. Envoy Gateway's request buffering task says it "requires Envoy to fully receive the request before forwarding it upstream" and "does not work with streaming or upgrade-based traffic such as gRPC streaming and WebSocket", where requests "can hang indefinitely". A BackendTrafficPolicy that ingress2gateway attached to a route serving WebSockets is a bug you did not have before. Check every generated policy against the routes it targets, and see WebSockets through a reverse proxy for what those routes need instead.
The surprise is client-body-buffer-size. In nginx that is the in-memory buffer for a request body before it spills to a temporary file; it is not a limit. ingress2gateway's README lists it as converted, and the Envoy Gateway emitter's source says "Prefer body max size if present, otherwise fall back to body buffer size". Tested, an Ingress with client-body-buffer-size: "1m" and no proxy-body-size produced requestBuffer.limit: 1Mi. A route that accepted 1 MiB in memory and up to the default limit on disk now rejects anything over 1 MiB with a 413. When both annotations are present, proxy-body-size wins and the tool warns that the buffer size is ignored.
proxy-buffer-size and proxy-buffering have no target in ingress2gateway at all: both produced "Unsupported annotation" warnings with the standard and Envoy Gateway emitters. They control nginx's response side. proxy-buffer-size sizes the buffer for the first part of the upstream response, which is where the upstream sent too big header error in header size limits comes from. Envoy does not buffer responses the way nginx does, as proxy buffering explains, so there is no field to copy the value into. The nearest Envoy Gateway setting, connection.bufferLimit (32768 bytes by default on both the client and the backend side), is a soft limit on connection buffers, not the same control, and it should not be raised just because an old annotation said 16k.
Redirects, headers and the prefix#
These are the annotations Gateway API core handles well.
ssl-redirect. ingress-nginx redirects to HTTPS with a 308 when the Ingress has TLS. ingress2gateway reproduces it as a separate HTTPRoute on the port 80 listener with aRequestRedirectfilter,scheme: https,statusCode: 308. Withssl-redirect: "false"the redirect route is not generated.RequestRedirectis Core support, so every conformant implementation honours it.upstream-vhost. Converted to aRequestHeaderModifierfilter that setsHost. Gateway API also has a purpose-built field,URLRewritehostname, described as "the value to be used to replace the Host header value during forwarding", but that filter is Extended support where the header modifier is Core.x-forwarded-prefix. The README says it is converted "when used alongsiderewrite-target", and the condition is strict. An Ingress withx-forwarded-prefix: "/app"and norewrite-targetproduced an HTTPRoute with no header and no warning. The same annotation next torewrite-targetproduced aURLRewritefilter plus aRequestHeaderModifiersettingX-Forwarded-Prefix: /app. If the application relies on the header, check for it in the output rather than in the log. Why the header matters, and why the application has to be told to read it, is in running an app under a subpath.
Real client IP: a ConfigMap key no converter reads#
use-forwarded-headers is not an Ingress annotation. It is an ingress-nginx ConfigMap key, default false, and when enabled nginx passes incoming X-Forwarded-* headers through instead of building its own. ingress2gateway converts Ingress objects and their annotations; its ingress-nginx provider has no code that reads the controller ConfigMap, and the same is true of compute-full-forwarded-for, the global proxy-body-size and the global timeouts. Cluster-wide settings have to be carried across by hand.
Envoy Gateway puts client IP trust in ClientTrafficPolicy, attached to the Gateway: with spec.clientIPDetection.xForwardedFor.numTrustedHops set, "the client IP is taken from the Nth address from the right end of the XFF header". Traefik puts it on the entryPoint with forwardedHeaders.trustedIPs, and its documentation recommends forwardedHeaders.insecure "only for tests purposes, not in production". Hop counting and CIDR trust fail in different ways when the topology changes, which configuring trusted proxies compares.
auth-url and configuration-snippet#
auth-url produced an "Unsupported annotation" warning from ingress2gateway. Gateway API v1.6.3 does define an ExternalAuth HTTPRoute filter, but it is marked <gateway:experimental>, so it exists only in the experimental channel CRDs. In Envoy Gateway the supported route is a SecurityPolicy with extAuth.http.backendRefs and headersToBackend; its documentation says a request "deemed unauthorized" is "denied with a 403 (Forbidden) response". Traefik's provider supports auth-url, auth-signin and auth-response-headers, with the caveats "Only URL and response headers copy supported" and "Forward auth behaves differently than NGINX". The difference that matters, what each proxy does when the auth service answers 302, is in nginx auth_request vs Traefik ForwardAuth vs Caddy forward_auth.
configuration-snippet was already disabled on a default install: allow-snippet-annotations defaults to false in the ConfigMap, and the annotation documentation warns that snippets "can be dangerous in multi-tenant clusters", citing CVE-2021-25742. ingress2gateway reports it as unsupported, and no Gateway API field can hold raw proxy configuration. Each line has to become an explicit filter or policy. Traefik is the exception: its provider accepts add_header, proxy_method, more_set_headers, proxy_set_header, more_set_input_headers, set, if and return code [text], with "minimal variable interpolation". Anything outside that list needs rewriting either way.
Which path to take#
Move to Gateway API with ingress2gateway when you want off the Ingress API as well as off the controller, and you have time to read every warning. Choose the emitter for your target implementation, then work through the output in this order: routes with no timeout annotation, every generated BackendTrafficPolicy against streaming routes, every client-body-buffer-size, x-forwarded-prefix without rewrite-target, and the ConfigMap keys nothing translated.
Swap to Traefik's kubernetesIngressNGINX provider when the immediate goal is to stop running unpatched software with the fewest manifest changes. It watches IngressClass nginx and controller k8s.io/ingress-nginx by default, and its provider options mirror the ConfigMap keys. Set them deliberately, because several defaults differ from what ingress-nginx shipped:
| Setting | ingress-nginx ConfigMap default | Traefik provider default |
|---|---|---|
| Connect timeout | 5 | proxyConnectTimeout 60 |
| Read and send timeouts | 60 | 60 |
| Request body limit | 1m | proxyBodySize 1048576 bytes |
| Response buffer size | 4k | proxyBufferSize 8192 bytes |
| Client body buffer | 8k | clientBodyBufferSize 16384 bytes |
| Request buffering | on | proxyrequestbuffering false |
| Retries | error timeout, 3 tries | error timeout, 3 tries |
Retries are not identical either: the documentation notes that "Unlike NGINX, Traefik does not guarantee that retries are sent to a different server". The wider trade between Envoy-based gateways and Traefik is in nginx vs HAProxy vs Envoy vs Caddy vs Traefik, and Traefik's own object model in Traefik routers, services and middlewares.
Where this stops applying#
Every field and default here is version specific. ingress2gateway behaviour was tested on v1.2.0 with --input-file and the standard and envoy-gateway emitters only; the x10 multiplier is a constant in the source and could change in a later release. Envoy Gateway's API is v1alpha1, and its main branch already adds a mode to requestBuffer that is not in v1.9.2. Other Gateway API implementations have their own policy resources and their own defaults, and the Envoy Gateway column does not apply to them: read the implementation's API reference for the exact field and default before relying on any value. Traefik behaviour is from its v3.7 documentation and was not tested here.
Frequently asked questions#
What replaces nginx.ingress.kubernetes.io/proxy-body-size in Gateway API?#
Nothing portable. Gateway API has no request body size field, so the limit moves into an implementation policy. In Envoy Gateway it is BackendTrafficPolicy spec.requestBuffer.limit, which returns a 413 above the limit and fully buffers the request, so keep it off WebSocket and gRPC streaming routes. ingress2gateway emits it only with --emitter=envoy-gateway (or another implementation emitter); the default standard emitter drops it with a warning.
What replaces proxy-read-timeout in Gateway API?#
timeouts.request on the HTTPRoute rule, with timeouts.backendRequest for a single attempt. They are total deadlines, not gaps between reads. ingress2gateway sets timeouts.request to the largest of the three timeout annotations multiplied by 10, so proxy-read-timeout: "60" becomes 10m0s.
Why do my requests time out at 15 seconds after moving to Envoy Gateway?#
Because the route has no timeouts.request and Envoy's default request timeout of 15 seconds applies. On ingress-nginx the same route had a 60 second read timeout from the ConfigMap. ingress2gateway only emits a timeout for Ingresses that carried a timeout annotation, so set one explicitly on any route that can take longer than 15 seconds.
Does ingress2gateway convert the ingress-nginx ConfigMap?#
No. It converts Ingress objects and their annotations. Global settings such as use-forwarded-headers, compute-full-forwarded-for, the cluster-wide proxy-body-size and the default timeouts have to be recreated by hand, for example in an Envoy Gateway ClientTrafficPolicy or BackendTrafficPolicy attached to the Gateway.
Is there a Gateway API equivalent of auth-url?#
Only in the experimental channel. Gateway API v1.6.3 defines an ExternalAuth HTTPRoute filter marked experimental. In practice you use the implementation's resource: a SecurityPolicy with extAuth in Envoy Gateway, or the auth-url annotation itself under Traefik's kubernetesIngressNGINX provider.
Primary sources#
Every normative claim on this page is checked against the specification or the vendor documentation listed here. Where behaviour is version dependent, the version is named in the text.
- Kubernetes blog, Ingress NGINX Retirement
- kubernetes/ingress-nginx repository (archived; releases)
- ingress-nginx, Annotations
- ingress-nginx, ConfigMap
- ingress2gateway, ingress-nginx provider README and source
- Gateway API, HTTPRoute types (timeouts, filters, ExternalAuth)
- Envoy Gateway, HTTP timeouts
- Envoy Gateway, request buffering
- Envoy Gateway, API types v1.9.2 (Timeout, BackendConnection, ClientConnection)
- Traefik, Kubernetes Ingress NGINX provider and annotation reference
Found something wrong, or behaviour that differs on your version? Report it with the version number and a primary source. Anything substantive is fixed in the page and logged on the corrections page. See editorial standards for how pages are researched, sourced and reviewed.
More in reverse proxy configuration#
- App under /app/ behind a proxy: X-Forwarded-Prefix
- Caddy reverse proxy
- nginx auth_request vs ForwardAuth: why the 302 fails
- gRPC through a reverse proxy
- HAProxy configuration for HTTP reverse proxying
- Health checks and upstream failover
- nginx proxy_pass and the trailing slash
- nginx as a reverse proxy: a complete configuration
- Sticky sessions and session affinity
- WebSockets through a reverse proxy