Our converted route answered a 20-second report with a 504 at 15.0 seconds, and none of the migration tool's warnings had mentioned a timeout on that route. That was a lab run: five ordinary Ingresses moved off ingress-nginx, which the Kubernetes project retired in March 2026. The tool built for the move, ingress2gateway, flagged every annotation it could not translate. What it never saw were the defaults nobody had written into an annotation, and those are what changed underneath us.
CloudSpinx builds and runs Kubernetes clusters for engineering teams in Kenya and across East Africa, and the ingress layer is part of what we build and hand over in our managed Kubernetes work, so we ran the move ourselves before advising anyone which alternative to pick. ingress2gateway 1.2.0 converted five Ingresses carrying 16 different ingress-nginx annotations, and Envoy Gateway 1.9.3, run in its standalone mode on Envoy 1.40.2, served the result to a test backend. We did not run ingress-nginx itself. What this post says about its behavior comes from its own documentation and config template at the final release.
Is ingress-nginx really end of life?
Yes. Maintenance stopped in March 2026, the last releases (controller v1.15.1, v1.14.5 and v1.13.9) shipped on 19 March, and the GitHub repository is archived read-only. The retirement notice says what that means in one line: "no further releases, no bugfixes, and no updates to resolve any security vulnerabilities that may be discovered."
Nothing breaks on the day, which is the problem. The controller keeps serving, so nobody raises a ticket. In most cases one command with cluster-admin rights tells you whether you are on it:
kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx
Microsoft and SUSE still patch their own builds, for a while, and Chainguard publishes a fork of the controller with security fixes backported. Microsoft's security patches for the ingress-nginx-based application routing add-on on AKS end in November 2026, and its migration guide points you to a Gateway API version of the same add-on or to Application Gateway for Containers. SUSE Rancher Prime LTS patches critical and important CVEs in RKE2 1.35's Ingress NGINX component through December 2027. F5's NGINX Ingress Controller is a separate project and is not part of this retirement.
Which ingress-nginx alternative to use
Gateway API for most clusters, and Traefik where k3s or RKE2 already runs it. There are three options, and they cost very different amounts of work.
Gateway API is where the Kubernetes project points. Its Ingress documentation says the Ingress API "has been frozen" and recommends Gateway instead. You choose an implementation and rewrite each Ingress as a Gateway plus HTTPRoutes, which ingress2gateway drafts for you, with extra output for Envoy Gateway, kgateway, agentgateway, Airlock and Google's GKE gateway. On AKS, Microsoft's replacement runs on an Istio control plane.
Another Ingress controller keeps your Ingress objects and swaps the thing reading them. On k3s, and on RKE2 clusters created from v1.36, that controller is already Traefik, installed by default. SUSE's supported route off ingress-nginx is Traefik with a compatibility layer that, in SUSE's words, "can interpret many existing Ingress NGINX annotations". We did not test that layer, so read "many" as a list to check against your own annotations.
Staying put has a date on it: November 2026 on AKS, December 2027 on RKE2 1.35 with a Prime LTS subscription, and none at all for the upstream controller you installed yourself, because the Kubernetes project no longer patches it. The controller is one more component on the upgrade calendar your cluster already owes, and an easier one to forget.

When the choice is ours, we would take Gateway API on Envoy Gateway. Its emitter translated 6 of our 16 annotations against 4 for the default output, and the auth and rate-limit annotations it dropped have somewhere to go by hand: Envoy Gateway's SecurityPolicy carries basic auth and external auth, and its BackendTrafficPolicy carries rate limits. On a small k3s or RKE2 cluster, Traefik is the shorter move because the distribution already ships it, and RKE2's guide runs it beside ingress-nginx before the switch.
What ingress2gateway dropped
Of the 16 annotations, the default output translated 4: the two CORS annotations, regex paths and the read timeout. The Envoy Gateway output also carried the body-size limit and the IP allowlist. The other 12, or 10, each came with a warning line. Nothing was dropped silently, and that is to the tool's credit.
A warning does not stop the YAML applying, though, and everything that applied served traffic.
- protection or routing lost
- behavior changed
The two outputs differ only in the body limit and the IP allowlist, the two annotations only the Envoy Gateway emitter translated. Basic auth, the login redirect, the rate limit and the rewrite were left for a person.
The admin route is the one to lose sleep over. It had an IP allowlist and basic auth, and the default output served it to a request from outside the allowlist with no password asked. The /reports route lost the external login in front of it under both outputs, and the API's limit of 10 requests a second is simply gone: nothing in either output limits that route. Treat every warning line, whatever its wording, as a route that cannot cut over yet.
On AKS, pass your class. Given our five Ingresses on the webapprouting.kubernetes.azure.com class and default flags, the tool printed "No resources found" and exited 0.
The defaults nobody wrote down
ingress2gateway reads Ingresses and Services. It never reads the controller's ConfigMap, where cluster-wide overrides live, and it cannot translate a default that was never written anywhere. Three of those changed in our run, and the one timeout we did write changed meaning.
A 15-second limit on every route you left alone
Envoy Gateway writes no timeout onto a route whose HTTPRoute has none, so Envoy's own default applies, and Envoy documents it as 15 seconds that last until "the upstream response has been completely processed". ingress-nginx's proxy-read-timeout defaults to 60 seconds and, per its own docs, counts only the gap "between two successive read operations, not for the transmission of the whole response".
- row delivered
- row never sent
ingress-nginx's documented default is a 60 second read timeout counted between reads, so neither request comes near it there. Envoy's 15 seconds runs until the whole response has arrived.
The report got its 504 at 15.0 seconds. The streamed export was worse. Through Envoy the client received an HTTP 200, then 15 of the 25 rows, then a reset, so anything that checks only the status code saw a successful download of a short file.
Setting timeouts.request: 60s on the route let the 20-second report through in our run, and a 70-second one then failed at 60 seconds, as it should. Give every route an explicit timeout, sized to its longest legitimate response, before the cutover rather than after the first complaint.
Longer timeouts where you set one
The tool turns ingress-nginx's connect, read and send timeouts into one request timeout of ten times the largest, and its warning calls that a best-effort translation without giving the factor. Our proxy-read-timeout: "120" became 20 minutes. A backend that sat silent for 130 seconds got its 200, where ingress-nginx's documented 120-second read timeout would have fired first, and a request to a hung worker can now wait 20 minutes.
Upload caps that disappeared
ingress-nginx caps a request body at 1m unless told otherwise. Where the output wrote no limit, Envoy Gateway enforced none in our tests: a 2 MiB upload went through on the route with no annotation, and with the default output so did 25 MiB on the route annotated 20m. The tool's note on that annotation reads "Most Gateway API implementations have reasonable body size and buffering defaults." If an upload form leaned on the old cap, it now leans on nothing. The Envoy Gateway output did carry 20m across, and there the 25 MiB request got its 413.
Headers your application may read
ingress-nginx's template sets X-Real-IP, X-Forwarded-Host, X-Forwarded-Port and X-Scheme on every proxied request. Behind Envoy our backend received none of them, only x-forwarded-for, x-forwarded-proto, x-request-id and Envoy's own x-envoy-external-address. Code that logs or rate-limits on X-Real-IP gets no header after the move.
Checking these, route by route, is the slow part of the move. If you would rather hand it over, send the Ingress export through the scoping form and we mark each route for you.
Moving to Gateway API without surprises
-
Inventory what you use, and save the controller's ConfigMap, because its overrides are invisible to ingress2gateway:
kubectl get ingress -A -o json \ | jq -r '.items[].metadata.annotations // {} | keys[] | select(startswith("nginx.ingress.kubernetes.io/"))' \ | sort | uniq -c | sort -rn -
Run ingress2gateway with your ingress class and the emitter for your implementation, and keep its warnings next to the YAML it wrote.
-
Write the dropped auth, allowlist and rate-limit rules by hand, and give every route an explicit request timeout and, where an upload cap mattered, a body limit.
-
Run both controllers side by side and test each hostname against the new one with
curl --resolvebefore DNS moves. -
Move one hostname at a time, with the record's TTL lowered a full TTL ahead, since resolvers can keep the old address for as long as the TTL allows. Leave ingress-nginx running until the last route is across.
A cluster on AKS with a few Ingresses and no annotations can follow Microsoft's guide on its own, checking timeouts on the way, and does not need us. An RKE2 1.35 cluster on Prime LTS has until December 2027, and should spend part of it testing Traefik's compatibility layer against its own annotations.
A controller installed from the upstream chart is an unpatched proxy in front of every service today. Send us kubectl get ingress -A -o yaml, the controller's ConfigMap and where the cluster runs, through the managed Kubernetes scoping form or on WhatsApp at +254 713 403 044. You get back every route marked translates, needs a rule written or needs a decision from you, the implementation we would use for your cluster, and one figure for the move. Reading it costs nothing and commits you to nothing. If you go ahead, we handle the move with your team, one hostname at a time.