Ingress ran out of road. Gateway API is the replacement, and the argument is the org chart
Gateway API is usually sold on features. The real reason to move is that Ingress has no separation between the team that owns the load balancer and the team that owns the route. Here is the migration that does not need a maintenance window.
Open the Ingress objects in a cluster that has been running for three years and count the annotations. The record we have seen on a single object is twenty-three: a rewrite target, a CORS block, a custom read timeout, a Lua snippet in configuration-snippet, session affinity, and a proxy buffer size someone tuned during an incident in 2022. None of it is in the Ingress spec. All of it is specific to one controller. There is no schema, no validation, and no way to know which annotations the controller silently ignored.
That bill comes due the day you change controller, and a lot of teams got that day handed to them when the ingress-nginx maintainers announced the project would be wound down rather than extended. Check the current status before you plan around it, but the direction is clear: the most deployed Ingress controller in the world is not the long-term answer, and the successor work is happening in Gateway API.
Gateway API is worth moving to anyway, and not mainly for the features. It is worth moving to because it splits one object into three, along the line that already exists in your organisation.
What Ingress actually failed at
The spec is too small. An Ingress can express a hostname, a path, a backend service and a TLS secret. Everything else — timeouts, retries, redirects, header manipulation, rate limits, canary weights, auth — lives in annotations. An annotation is an untyped string. It is not validated at admission, not versioned, and not portable.
There is no separation of roles. One object holds the hostname claim, the TLS certificate reference and the routing rules, and it is owned by whoever has write access to the namespace. Nothing stops two teams claiming the same hostname. When they do, conflict resolution is undefined by the spec and implementation-specific in practice, often "oldest object wins", silently.
Routing stops at host and path. No header matching, no method matching, no query parameter matching, no weighted split between two backends. Every canary deployment therefore becomes another annotation, or a second product.
The resource model is the argument
Gateway API replaces the single object with three, each owned by a different role.
GatewayClass is cluster-scoped and behaves like StorageClass. It says "this kind of gateway is implemented by this controller". Whoever provides the data plane installs it, which in a managed cloud is usually the provider.
Gateway is namespaced and belongs to the platform team. It declares listeners: port, protocol, hostname, TLS certificate. Critically, it declares allowedRoutes — which namespaces are permitted to attach routes to this listener, selected by label. The platform team owns the certificate, the public address, the WAF in front of it and the hostname policy.
HTTPRoute is namespaced and belongs to the application team, in their own namespace. It declares matches (path, header, method, query), filters (redirects, rewrites, request and response header modification, mirroring) and backendRefs with weights. GRPCRoute does the same for gRPC.
Two smaller things make a large difference in practice. ReferenceGrant requires the owning namespace to explicitly consent before a route in another namespace can point at its Service or Secret, which closes the confused-deputy path that Ingress left open. And both Gateway and Route report real status conditions — Accepted, ResolvedRefs, Programmed — so when your configuration is wrong, the API tells you, instead of the controller logging a line nobody reads.
The result is that the platform team can hand application teams a self-service routing surface without handing them cluster-admin, and without a ticket per hostname. That is the same boundary problem as running several teams on one cluster, solved with the same instinct.
Where support actually stands
The core set — GatewayClass, Gateway, HTTPRoute — reached v1 in the standard channel, and GRPCRoute followed. The L4 route types, TCPRoute, UDPRoute and TLSRoute, have stayed in the experimental channel much longer, so if your traffic is mostly not HTTP, check which channel the feature you need is in before you design around it.
Implementations worth shortlisting: Envoy Gateway, Istio (which serves both north-south and, through the GAMMA work, east-west mesh routing with the same route types), Cilium, Contour, Traefik, Kong and NGINX Gateway Fabric. On managed clouds, Google has a first-party Gateway controller that maps to its load balancers including multi-cluster gateways, AWS exposes Gateway API through both its Load Balancer Controller and a VPC Lattice controller, and Azure through Application Gateway for Containers. Feature coverage differs a lot between them. Every implementation publishes a conformance report per version; read the one for the exact version you intend to run rather than the marketing page.
Migrating without a big bang
Ingress and Gateway API coexist. Different CRDs, different controllers, same cluster.
- Install the Gateway API CRDs and one controller alongside the existing one. The CRDs are cluster-scoped and shared, so decide the version and the channel deliberately and put them under the same change control as anything else cluster-wide.
- The platform team creates one shared Gateway per external-facing class, with the certificate and the hostname suffix, and an
allowedRouteslabel selector. - Move one low-traffic service. The
ingress2gatewayCLI converts the spec, but it converts only what is in the spec. The annotations are the actual migration, and each one has to be re-expressed as a filter, a spec field, or an implementation-specific policy. - Shift traffic at DNS or at the load balancer, not by deleting the Ingress. Keep the old object until you have a week of clean latency and error metrics.
- Work through the estate one team at a time. Anything carrying a Lua snippet goes last.
Weighted backendRefs are native, which is why both Argo Rollouts and Flagger have Gateway API providers. If you are already doing progressive delivery, this removes the controller-specific canary annotations from that path too.
The things people forget
- CRD ownership is shared. Two controllers in one cluster use one set of CRDs. Decide who upgrades them and pin the version, or an upgrade of one controller breaks the other.
- Policy attachment is the unfinished part. Some things are now in the spec, such as HTTPRoute timeouts. Retries, rate limits, mTLS to the backend and authentication are still implementation-specific policy CRDs. You are still writing per-implementation config, only now it is typed and reviewable instead of a string.
- RBAC has to change with the model. Splitting the objects buys nothing if application teams still have edit rights on Gateways.
- cert-manager and external-dns both support Gateway resources, but on specific versions and with different annotations from the Ingress path. Verify before you cut over.
- One Gateway per team means one cloud load balancer per team. A shared Gateway with per-team hostname listeners is materially cheaper and is the default we recommend.
When it is not worth moving yet
If you have fewer than ten Ingress objects, one team, no canary requirement and a controller with a maintained future, the migration is cost without benefit this quarter. Same if your traffic is mostly raw TCP, or if the managed offering on your cloud is behind on the features you rely on. In that case do the cheap half: stop adding snippet annotations, and make sure the controller you run is one that will still be maintained in two years. We work through this in the platform phase of a cloud engagement.
What to do this week
Run kubectl get ingress -A -o json and count annotations per object, sorted descending. The top five are your real migration plan, and the count tells you honestly how much of your routing logic is currently unportable.