In March 2026, AWS did something it almost never does: it told you to stop configuring its load balancers its way. The AWS Load Balancer Controller v3.0.0 release made Gateway API support generally available, and the announcement post by Alexandra Huides and Zac Nixon frames the standard Kubernetes Gateway API — not the controller's own Ingress annotations — as the way to define load balancing on EKS going forward. When the company that invented the annotation-driven load balancer integration decides the typed, portable API is the primary interface, the "should our fleet bet on Gateway API?" debate is effectively over. The only question left is which implementation you bet with.
Here is the news in 60 seconds, with the one fact-box that matters:
- What shipped: AWS Load Balancer Controller v3.0.0 (March 2026) graduated Gateway API routing to GA. Layer-7 routes (
HTTPRoute,GRPCRoute, supported since v2.14) provision an ALB via theGatewayClasscontrollergateway.k8s.aws/alb; Layer-4 routes (TCPRoute,UDPRoute,TLSRoute, supported since v2.13.3) provision an NLB viagateway.k8s.aws/nlb. Both are behind theALBGatewayAPI/NLBGatewayAPIfeature gates. - The one hard limit: you cannot mix protocol layers on a single Gateway — a
TCPRouteand anHTTPRouteon the same Gateway is not supported. L4 and L7 live on separate Gateways, hence separate load balancers. - Why now: Gateway API v1.5 (February 2026) gave vendors a stable target —
GRPCRoutein the standard channel, Gateway merging, and CORS filters graduated. Vendors GA against stable specs; March follows February for a reason. - Context, in two sentences: Ingress NGINX — the default ingress for a generation of clusters — was retired in November 2025 with maintenance halting in March 2026 and no further CVE patches, and SIG Network tells every user to migrate to Gateway API or another controller. AWS even published its own NGINX-to-AWS-LBC migration guide whose stated end state is Gateway API resources — the hyperscaler is migrating you twice, and the second hop is the standard.
And here is the portability proof, up front where it belongs. One realistic route — hostname, path, and header match, TLS terminated at the Gateway listener:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-route
spec:
parentRefs:
- name: tenant-gateway
hostnames: ["api.example.com"]
rules:
- matches:
- path: { type: PathPrefix, value: /v2 }
headers:
- name: X-Tenant-Tier
value: paid
backendRefs:
- name: api-v2
port: 8080| Same YAML, three data planes | What serves it | What you changed |
|---|---|---|
| AWS Load Balancer Controller | Managed ALB, GatewayClass controller gateway.k8s.aws/alb | gatewayClassName on the Gateway |
| Envoy Gateway | Envoy proxy Deployment in-cluster | gatewayClassName on the Gateway |
| Cilium Gateway API | eBPF dataplane, no sidecar proxy pods | gatewayClassName on the Gateway |
The route — the part your developers write — is identical. The one line that changes names the infrastructure that serves it. That is the whole argument for the standard in a single table: your routing intent is now an asset that survives a change of load balancer, cloud, or decade.
What v1.5 standardized, and why AWS could GA against it
Vendors do not graduate integrations against moving targets. Gateway API v1.5, released February 2026, is the stable floor under AWS's March GA, and three graduations in it are the ones that matter to a platform team.
First, GRPCRoute in the standard channel. gRPC is no longer the second-class citizen that needed an annotation or a vendor extension — it is a core route type, which is why the LBC can promise "L7 means HTTPRoute and GRPCRoute" without hedging. For a fleet running agent sandboxes and internal gRPC services next to web tenants, that single graduation removes the "but what about our gRPC traffic?" objection to standardizing.
Second, Gateway merging. Multiple Gateways can now merge onto shared listeners without conflict, which is the primitive that makes the role split real: the platform team owns the listener (port 443, the TLS cert), tenant teams attach routes to it, and nobody steps on anyone else's config. Without merging, "shared ingress, separate teams" devolves into either one giant shared Gateway file or one load balancer per tenant — the two failure modes every multi-tenant ingress story eventually hits.
Third, CORS filters as standard. Cross-origin policy is the kind of thing that previously lived in an annotation (cors-allow-origin and friends, spelled differently per controller, silently ignored when misspelled) and is now a typed filter the apiserver validates. Small feature, large signal: the boring, everyday knobs are finishing their migration from strings to schema.
The pattern to internalize: AWS GA'd in March because the spec went stable in February. Track the spec's graduation calendar and you can predict vendor behavior a quarter ahead — a cheaper planning input than waiting for each vendor's changelog.
What annotations cost you
The old way of getting "route /v2 for paid tenants to the new backend" out of the AWS controller looked like this on an Ingress object:
metadata:
annotations:
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
alb.ingress.kubernetes.io/ssl-redirect: '443'
alb.ingress.kubernetes.io/conditions.api-v2: >
[{"field":"http-header","httpHeaderConfig":{"httpHeaderName":"X-Tenant-Tier","values":["paid"]}}]Five annotations, JSON embedded in YAML embedded in a string map, one typo away from silence. That last property is the actual cost: a misspelled annotation is not an error, it is a no-op. alb.ingress.kubernetes.io/condtions.api-v2 applies cleanly, reconciles successfully, and routes nothing — the controller never knew you meant something. The equivalent mistake in the HTTPRoute above (header instead of headers, a bad path type) is rejected by the apiserver before anything is persisted, with a validation error pointing at the field.
To be precise about the title claim: AWS has not removed the annotation path — existing Ingress and Service integrations still work, and the migration guide's first hop is NGINX-to-LBC-on-Ingress. "Doesn't default to annotations anymore" means the new capability, the documented end state, and the worked examples are all Gateway API. The annotation surface is maintenance mode in everything but name: no new routing expressive-ness will ever land there, because every new feature (merging, CORS filters, GRPCRoute) is being built on the typed side. Betting new config on annotations in 2026 is betting on the branch that receives no investment.
The honest limits: what GA does not mean
A GA stamp on one controller is not portability magic. Four pieces of residue stay non-portable, and a fleet plan has to name them:
1. The L4/L7 split forces two Gateways on AWS. Because mixing TCPRoute and HTTPRoute on one Gateway is unsupported, a tenant needing both Postgres-over-TCP and HTTPS gets two Gateways and two load balancers (NLB + ALB), each billed. Envoy Gateway and Cilium have no such restriction — one Gateway can serve both. This is the sharpest edge in a GatewayClass swap: moving that tenant's YAML from AWS to self-hosted deletes a load balancer, and moving back creates one. Cost-model it per tenant before you promise "seamless portability."
2. AWS-only knobs live in AWS-only CRDs. Anything the standard cannot express — target-group health-check tuning, ALB attributes, listener-rule ordering — lives in the gateway.k8s.aws/v1beta1 extension CRDs (LoadBalancerConfiguration, TargetGroupConfiguration, ListenerRuleConfiguration). These are typed and validated, which beats annotations, but they are not portable: that block of YAML is dead weight on Envoy or Cilium. Discipline: keep cloud-only tuning in extension CRDs attached by reference, never smeared across the portable route — so the day you move, you drop one file, not fifty edits.
3. Channel discipline bites. Gateway API ships a standard channel and an experimental channel, and controllers disagree on which they install. The failure mode is real: a controller that watches a standard-channel CRD (say, ListenerSet) crash-loops when the cluster only has the experimental XListenerSet installed — a version skew between your CRD bundle and your controller version, and nothing routes. Pin the CRD channel in the same commit as the controller version, and upgrade them as one unit. The conformance reports each implementation publishes are versioned for exactly this reason — read the report for your version, not the project's latest.
4. Route support varies by implementation and version. AWS documents its matrix precisely (L7 from v2.14, L4 from v2.13.3). Envoy Gateway, Cilium, Traefik, and the rest each have their own matrix and their own conformance badges, and "supports Gateway API" without a version number is marketing, not engineering. Before standardizing, open the implementation's conformance report and check the four route types you actually need at the version you will actually run.
Envoy Gateway vs Cilium: the fleet pick as a table
For a self-hosted, Cluster-API-managed fleet — owned Hetzner machines, no cloud load-balancer API — the choice narrows to two serious defaults. Same HTTPRoute, different trade-offs:
| Dimension | Envoy Gateway | Cilium Gateway API |
|---|---|---|
| Data-plane model | Dedicated Envoy proxy Deployment per Gateway; familiar L7 feature surface (rate limit, auth, transcoding) | eBPF in the kernel via the CNI you may already run; no proxy pods per Gateway |
| Getting an address on bare metal | Needs an address allocator: MetalLB/equivalent or host networking; you operate the LB layer | Same bare-metal address problem, but if Cilium is already your CNI the dataplane half is sunk cost |
| gRPC + conformance posture | Envoy is the reference-grade gRPC proxy; consistently top-tier conformance scores | Needs gatewayAPI.enableAlpn=true for gRPC (easy to miss); strong but check the versioned report |
| Day-2 cost for a small team | One more stateful-ish Deployment to size, upgrade, and scale per Gateway | If Cilium is already the CNI: near-zero marginal ops; if not, adopting a CNI and an ingress at once is the bigger project |
The verdict rule: if Cilium is already your CNI, take Cilium Gateway — the marginal ops cost is ~zero. Otherwise, take Envoy Gateway — the standalone ingress with the best gRPC story and the simplest mental model. Either way, the portable asset is the HTTPRoute YAML your tenants write, not the proxy that serves it. Revisit only when a tenant needs something outside the standard (then see limit #2), not on every release.
What to do Monday
- Pin standard-channel CRDs with the controller version, in one commit, upgraded as one unit. Channel skew is the cheapest outage in this space and the most embarrassing.
- One GatewayClass per environment (
prod-alb/prod-envoy), referenced by name from tenant Gateways — never let tenants name controllers directly. The class is your seam for moving data planes later. - Keep cloud-only knobs in extension CRDs, referenced, not inlined — so a future move drops files instead of editing routes.
- Read the conformance report for your exact version and check the four route types you need. "Supports Gateway API" is not a support matrix.
The signal AWS sent in March is not "use our controller." It is that the typed, portable API won — the hyperscaler with the most to gain from lock-in chose the standard as its primary interface. For a self-hosted fleet, that converts Gateway API from a CNCF spec waiting on endorsement into the durable layer to build tenant routing on: write routes once, swap the GatewayClass when the infrastructure changes, and spend the saved migration budget on something tenants actually notice.
Sources
- AWS Load Balancer Controller v3.0.0 release: Gateway API support GA for ALB and NLB — github.com/kubernetes-sigs/aws-load-balancer-controller/releases/tag/v3.0.0
- Huides & Nixon (2026-03-06): "AWS Load Balancer Controller adds general availability support for Kubernetes Gateway API," incl.
gateway.k8s.aws/albGatewayClass example and role-oriented design rationale — aws.amazon.com - LBC v3.0 Gateway API overview: L4 routes (TCPRoute, UDPRoute, TLSRoute ≥ v2.13.3) vs L7 routes (HTTPRoute, GRPCRoute ≥ v2.14), no mixed-protocol Gateways,
NLBGatewayAPI/ALBGatewayAPIfeature gates — kubernetes-sigs.github.io - LBC v3.0 L4 routing:
controllerName: gateway.k8s.aws/nlb, reconciliation loop,gateway.k8s.aws/v1beta1customization CRDs — kubernetes-sigs.github.io - Gateway API v1.5 standard channel (GatewayClass, Gateway, HTTPRoute, GRPCRoute, ReferenceGrant, BackendTLSPolicy) and v1.6 TCPRoute/UDPRoute promotion — gateway-api.sigs.k8s.io via implementation docs
- Ingress NGINX retirement (announced Nov 2025, maintenance ends March 2026, migrate to Gateway API) — kubernetes.io, TechTarget
- AWS NGINX-retirement migration guide (NGINX → LBC on Ingress → Gateway API) — aws.amazon.com
- Self-hosted implementation landscape (Envoy Gateway, Cilium, Istio, Traefik, Contour conformance; Cilium
enableAlpngRPC note; version snapshots mid-2026) — Gateway API implementations list and community migration write-ups
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Tenant routing on a self-hosted PaaS is exactly the Gateway API bet this post describes: portable routes up front, your machines underneath. Star the repo on GitHub or deploy your first app today.



