Skip to main content

Ingress-NGINX Is Retired. Your Annotations Aren't Portable: A Gateway API Migration Audit

10 min readDora NodaDora Noda
Share
On this page

In November 2025, Kubernetes SIG Network and the Security Response Committee announced the retirement of Ingress NGINX: best-effort maintenance until March 2026, then no more releases, no more bugfixes, and no more security patches. That deadline has now passed. Existing deployments keep running and the artifacts stay available, but every month you remain on ingress-nginx is a month on an unpatched edge proxy — and one widely cited migration guide puts Datadog telemetry at roughly 50% of cloud-native clusters still running it.

So here is the verdict up front, including a correction to this post's own working title: Gateway API v1.5 did not add native WebSocket and retry semantics. Released February 27, 2026 and billed as the project's biggest release, v1.5 moved six features to the Standard channel — Gateway client-certificate validation, certificate selection for Gateway TLS origination, ListenerSet, TLSRoute v1, ReferenceGrant v1, and Gateway/HTTPRoute-level authentication. Retry and session persistence are still Experimental. If you planned your migration around a changelog headline instead of the release notes, re-plan it around the table below — that table is the whole post in one artifact:

NGINX annotation you rely onGateway API equivalentMaturity todayVerdict
proxy-send/read/connect-timeoutHTTPRoute timeouts (request, backendRequest)StandardClean move
canary-weight, canary-by-headerHTTPRoute backendRefs weights; header match + weightsCoreClean move — canary gets simpler
proxy-set-headers, auth-type: basicHTTPRoute filters (RequestHeaderModifier, RequestRedirect, URLRewrite); route-level authCore / Standard since v1.5Clean move — this is the other thing v1.5 actually gave you
enable-rewrite-log, rewrite-targetURLRewrite filterStandardClean move
affinity: cookie (sticky sessions)HTTPRoute sessionPersistenceExperimentalWorks, but you are betting on a pre-GA API (the idleTimeout field was recently removed from it)
proxy-next-upstream, retry tuningHTTPRoute retry (GEP-1731)Experimental, ExtendedNo Standard equivalent — use a vendor policy CRD or accept the gap
WebSocket proxyingPlain HTTPRoute, no special configCore behavior, implementation-defined edgesMostly free, with one sharp edge (below)
cors-*, rate-limit-*, whitelist-source-rangeNo Standard equivalentVendor policy CRD onlyPortable YAML ends here — this is the new lock-in
TLS passthroughTLSRouteStandard since v1.5Clean move — this is what v1.5 actually gave you
Raw TCP/UDP routingTCPRoute, UDPRouteStandard since v1.6 (June 30, 2026)Clean move if you track v1.6
configuration-snippet, server-snippet, Lua pluginsNothingNo equivalent by designRewrite or drop — the tool will only warn you

Two things to read off this table before the detail. First, the top half is genuinely good news: timeouts, canary weights, rewrites, and header manipulation — the bulk of what a git-push PaaS sets per tenant — all have first-class, portable equivalents. Second, the bottom half is the real migration cost, and it concentrates in exactly the annotations that made NGINX feel infinitely flexible: embed-arbitrary-config escape hatches and per-route operational policy. Gateway API standardizes the former out of existence and delegates the latter to implementation-specific policy CRDs. You are not trading one portable API for another; you are trading annotations for policies, and you should pick the policy author deliberately.

The three annotations from the title, honestly graded

Canary weights: the easy win. Weighted backendRefs in HTTPRoute are Core — stable, portable across every conformant implementation. An NGINX canary-weight: "10" plus canary-by-header pair becomes two backendRefs (stable 90, canary 10) with a header match rule. If anything, the Gateway API version is easier to audit than the annotation pair, because the split is data in the route rather than behavior hidden in controller-specific annotation semantics. Migrate these first; they build confidence for the harder rows.

WebSocket: free, except for the timeout interaction. Gateway API needs no WebSocket-specific resource — an HTTP upgrade is just an HTTP route, and it works. The sharp edge is timeouts: a request timeout that is perfectly sensible for REST traffic will sever a long-lived console or agent-streaming session mid-connection, because to the route it looks like one very slow request. The failure mode operators actually hit is subtler — one implementation routes WebSocket upgrades only when the Service port advertises a WebSocket appProtocol, so a migration that copies the Service YAML verbatim can silently downgrade upgrades to plain GETs. Checklist item for every migrated route that upgrades: confirm the upgrade end-to-end (not just the HTTP handshake) and scope request timeouts away from upgraded routes, using backendRequest or implementation idle-timeout knobs instead.

Retry tuning: the gap v1.5 did not close. proxy-next-upstream and its retry-attempt companions map to HTTPRoute retry under GEP-1731 — still Experimental, still Extended, meaning implementations may support it but nothing portable is guaranteed. In practice this is where you pick a GatewayClass whose policy CRD you are willing to marry: Envoy Gateway's BackendTrafficPolicy covers retries, timeouts, rate limiting, and circuit breaking; its SecurityPolicy covers JWT, CORS, and OIDC. That is a capable answer, and it is also new lock-in wearing a trench coat — the policy object only exists on that implementation. Budget for this explicitly: enumerate every route that sets retry annotations today, decide per route whether Standard-channel behavior suffices, and treat each remaining one as adopting a vendor API with the same eyes-open attitude you once had toward NGINX annotations.

The row that ends negotiations: snippets and Lua. configuration-snippet, server-snippet, and embedded Lua have no Gateway API equivalent by design — the whole point of the standard is that routing intent is structured data, not injected config text. ingress2gateway flags these as warnings and moves on. Every snippet needs a human decision: re-express as filters/policies, push the logic into the app, or drop it. In our experience reviewing platform migrations, this row — not the route table — is where the calendar goes. Count your snippets before you promise a date.

What ingress2gateway 1.0 actually does for you

The SIG Network migration tool hit 1.0 on March 20, 2026, deliberately timed days before the maintenance window closed — and the version number matters. Where the pre-1.0 tool converted three ingress-nginx annotations, 1.0 covers 30+, including CORS, backend TLS, regex matching, path rewrite, and timeouts. Run it; it is the correct first pass.

But treat its output as a draft, not a migration, for three documented reasons. First, translation is best-effort by design: the project's own announcement shows proxy-send/read-timeout annotations becoming a 10-second request timeout — a reasonable guess, not a preserved semantic. Second, implementation-specific behavior (custom Lua, config snippets) surfaces only as warnings, which in a thousand-route fleet is another way of saying it surfaces as a list nobody reads. Third, the tool converts static YAML; if any of your Ingress objects are constructed at runtime — a controller, an operator, platform code that templates routes per tenant — there is no YAML to convert, and you are rewriting Go types and client calls, a gap the Konveyor project's 2026 Ingress-to-Gateway rules specifically target. Complement the tool with Saiyam Pathak's ing-switch, which scores 119 ingress-nginx annotations by impact (NONE/LOW/MEDIUM) across the full scan-to-cleanup lifecycle, and read its MEDIUM list as your actual work plan.

Picking the implementation on hardware you own

On a self-hosted fleet there is no cloud load-balancer controller to default to, so the GatewayClass decision is yours and it sticks — the policy CRDs from the previous section come bundled with it. Three serious answers cover most owned-hardware fleets:

  • Envoy Gateway for the broadest policy surface (the BackendTrafficPolicy/SecurityPolicy pair above) and the most complete Extended-feature coverage, including experimental retry. The price is conceptual weight: Gateway, policies, and Envoy concepts to train your team on.
  • Cilium if you already run it as CNI, since Gateway API support rides on the eBPF datapath you operate anyway — one fewer proxy tier, and the CNCF's own migration guidance names Cilium Ingress among the suggested landing zones.
  • Traefik if annotation-parity speed matters most: the vendor claims over 90% ingress-nginx annotation coverage as a drop-in, which is a marketing number but a directionally useful one for fleets whose snippet count from the previous section came back terrifying. Treat it as a bridge — fast off the retired controller, Gateway API routes at your own pace.

Whichever you pick, pin the Gateway API CRD channel deliberately. v1.5 Standard is the sensible floor (TLSRoute v1, ReferenceGrant v1), v1.6 Standard adds TCP/UDP routes, and anything retry- or session-persistence-shaped requires the Experimental channel installed alongside — with the v1.5 migration caveat that Experimental TLSRoutes stored as v1alpha2/v1alpha3 are not usable under Standard-only installs. This is a two-line install decision that causes multi-hour debugging sessions when gotten wrong; write it in your runbook, not just your shell history.

The cutover sequence that avoids a routing gap

  1. Inventory before anything moves. Export every Ingress object and every NGINX annotation in the fleet; run the impact scoring; count snippets. No date promised until the MEDIUM list is sized.
  2. Install the new control plane beside the old one. Different GatewayClass/ingress class, same backends. Both controllers serve; only one receives production traffic.
  3. Convert, then hand-verify the warnings. Run ingress2gateway, then work the warning list as tracked tasks — each snippet/Lua block gets an owner and a re-expression decision.
  4. Mirror and shadow-verify. Duplicate routes on the new Gateway, replay or mirror production traffic, and check the three behaviors that break silently: WebSocket upgrades end-to-end, timeout behavior on long-lived connections, and retry counts under induced 5xx.
  5. Shift traffic per hostname, not per fleet. Move one tenant hostname at a time (DNS or parentRef), watch error budgets per route, keep the NGINX path hot as instant rollback.
  6. Decommission only after a full patch cycle. The retired controller gets no security fixes now — but deleting it the same day you cut over trades a known risk for an untested rollback path. One quiet interval, then remove.

The through-line of all six steps: at no point should a tenant's traffic depend on a translation you have not observed working. Annotations were executable folklore — comments that ran the edge. The migration is not converting YAML; it is re-deriving, per route, what the folklore actually did, and writing it down in an API that outlives its author.

The clock already ran out — the plan hasn't

Ingress-nginx retirement is not upcoming; maintenance ended in March 2026, and the unpatched-proxy months are accumulating. The good news, and it is real: weighted canaries, timeouts, rewrites, TLS passthrough, and (since v1.6) raw TCP/UDP all have stable, portable Gateway API homes, and the conversion tooling finally covers the common annotations. The honest cost sits in three places — experimental retry and session persistence, vendor policy CRDs for everything CORS/rate-limit-shaped, and the snippet inventory nobody wants to count. Size those three, cut over hostname by hostname, and the fleet lands on an API with a future instead of a controller without one.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex