If you’ve been running Kubernetes since 1.1 (hello, 2015), Ingress was the answer to “how do I expose my service to the outside world?” You got a resource, you pointed it at your Service, maybe threw in a few nginx.ingress.kubernetes.io/rewrite-target annotations when you got stuck at 2 AM, and you called it done.
Ingress still works. But it’s become the software equivalent of a old pickup truck held together with duct tape and vibes. Gateway API (generally available since late 2023) is the modern sedan — proper role separation, native traffic splitting, multi-protocol support, and drastically fewer annotations.
Here’s the honest assessment: when to jump ship, when to stay put, and why Kubernetes added a whole new resource family instead of just patching Ingress.
Why Ingress Became Annotation Soup
Let’s rewind. Kubernetes 1.1 (2015) gave us Ingress: a simple resource that said “route HTTP(S) traffic from the outside to my Services.” The design was intentionally minimal — just host rules, path rules, and a TLS cert.
But minimalism isn’t free. When folks wanted:
- Header-based routing
- Traffic splitting for canaries
- Cross-namespace routing
- TCP load balancing
- Different auth per route
- Rate limiting
- CORS headers
…they couldn’t change the Ingress spec without breaking older clusters. So controller implementations (nginx-ingress, Traefik, HAProxy Ingress) invented their own escape hatch: annotations.
Now a real Ingress looks like this:
apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: web annotations: nginx.ingress.kubernetes.io/rewrite-target: / nginx.ingress.kubernetes.io/rate-limit: "10" nginx.ingress.kubernetes.io/enable-cors: "true" nginx.ingress.kubernetes.io/cors-allow-origin: "https://example.com" nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/auth-type: "basic" nginx.ingress.kubernetes.io/auth-secret: "basic-auth" nginx.ingress.kubernetes.io/force-ssl-redirect: "true" cert-manager.io/cluster-issuer: "letsencrypt-prod"spec: rules: - host: api.example.com http: paths: - path: /v1 pathType: Prefix backend: service: name: api-v1 port: number: 8080 - host: api.example.com http: paths: - path: /v2 pathType: Prefix backend: service: name: api-v2 port: number: 8080That’s not routing config, that’s a Post-it note explosion on someone’s monitor. And if you switch from nginx-ingress to Traefik? Half those annotations become garbage — different controller, different annotation keys, different semantics.
What Gateway API Fixed
The Kubernetes community decided: instead of patching Ingress, let’s design routing right this time. Gateway API splits traffic routing into three composable resource types, each with its own owner.
GatewayClass — Who runs the load balancer? (Cluster admin) Gateway — How is it configured? (Cluster admin / platform team) HTTPRoute, TCPRoute, TLSRoute, GRPCRoute — Who accesses it? (App developers)
This is a massive shift. Under Ingress, one person (usually ops) owned the whole thing. Under Gateway API, there are clear boundaries:
GatewayClass (cluster admin: "I install Cilium as the ingress controller") └── Gateway (platform team: "I provision a load balancer with 2 replicas") └── HTTPRoute (app dev: "I want traffic for my API") └── HTTPRoute (app dev: "I want traffic for my web frontend") └── TCPRoute (database team: "I want raw TCP for Postgres")No annotations. Structured, extensible, versioned.
Resource Types: The Gateway API Family
GatewayClass
Tells Kubernetes: “This is the ingress controller brand we’re using.”
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata: name: cilium-gatewayspec: controllerName: io.cilium/gateway-controller description: "Cilium load balancer, installed by platform team"Once. Cluster-wide. Done.
Gateway
The actual load balancer instance(s). Ops/platform team owns this — they decide listeners, TLS, and IP ranges.
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata: name: production-gateway namespace: ingress-ciliumspec: gatewayClassName: cilium-gateway listeners: - name: http protocol: HTTP port: 80 - name: https protocol: HTTPS port: 443 tls: mode: Terminate certificateRefs: - name: production-cert kind: SecretThis lives in an ingress-cilium namespace, managed by ops. Developers never touch it.
HTTPRoute, TCPRoute, GRPCRoute, TLSRoute
App developers create these. They say “I want traffic matching these conditions routed to my service.”
Here’s an HTTPRoute equivalent to our Ingress example above:
apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: api-routes namespace: defaultspec: parentRefs: - name: production-gateway namespace: ingress-cilium hostnames: - "api.example.com" rules: - matches: - path: type: PathPrefix value: /v1 backendRefs: - name: api-v1 port: 8080 - matches: - path: type: PathPrefix value: /v2 backendRefs: - name: api-v2 port: 8080No annotations. No controller-specific hacks. Just spec.
Side-by-Side: Ingress vs HTTPRoute
Same routing logic, two approaches. Ingress lives in the ingress namespace:
apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: api namespace: ingress-nginx annotations: cert-manager.io/cluster-issuer: "letsencrypt-prod" nginx.ingress.kubernetes.io/ssl-redirect: "true"spec: ingressClassName: nginx tls: - hosts: - api.example.com secretName: api-tls rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: service: name: api-service port: number: 8080HTTPRoute lives with the application (same namespace as the Service):
apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: api namespace: default # Alongside the appspec: parentRefs: - name: production-gateway namespace: ingress-cilium hostnames: - "api.example.com" rules: - matches: - path: type: PathPrefix value: / backendRefs: - name: api-service port: 8080TLS is handled at the Gateway level (shared infrastructure), not repeated per route. The HTTPRoute author just says “I want traffic on this hostname” — the platform team already provisioned certs.
Traffic Splitting & Canary Deployments
This is where Gateway API shines. Native traffic splitting without sidecar hacks or annotations.
Canary: 90% to stable, 10% to new version:
apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: api-canary namespace: defaultspec: parentRefs: - name: production-gateway hostnames: - "api.example.com" rules: - backendRefs: - name: api-stable port: 8080 weight: 90 - name: api-canary port: 8080 weight: 10Ingress? You’d need external traffic splitting, a service mesh, or annotations (each controller has its own). Gateway API has it built in.
Header-based routing (route admins to a debug version):
apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: api-debug namespace: defaultspec: parentRefs: - name: production-gateway hostnames: - "api.example.com" rules: - matches: - headers: - name: X-Debug value: "true" backendRefs: - name: api-debug port: 8080 - backendRefs: - name: api-stable port: 8080Again: no annotations, native semantics.
Cross-Namespace Routing & ReferenceGrant
One of Ingress’s biggest gaps: you can’t safely route traffic across namespaces. An Ingress in namespace A can point to a Service in namespace B, but there’s no way for namespace B to allow it. A rogue HTTPRoute author could drain traffic from your database Service.
Gateway API fixes this with ReferenceGrant:
apiVersion: gateway.networking.k8s.io/v1beta1kind: ReferenceGrantmetadata: name: allow-api-routes namespace: databases # The namespace being referencedspec: from: - group: gateway.networking.k8s.io kind: HTTPRoute namespace: default # Only HTTPRoutes in 'default' namespace to: - group: "" kind: Service name: postgres-primaryNow an HTTPRoute in default can reference the postgres-primary Service in databases, but only because namespace databases explicitly granted it. Multi-tenancy actually works.
L4 Traffic: TCPRoute & TLSRoute
Ingress is HTTP/HTTPS only. Gaming servers, Postgres, Redis, raw TLS passthrough? You were out of luck.
Gateway API has TCPRoute for raw L4:
apiVersion: gateway.networking.k8s.io/v1alpha2kind: TCPRoutemetadata: name: postgres namespace: databasesspec: parentRefs: - name: production-gateway rules: - backendRefs: - name: postgres-primary port: 5432And TLSRoute for TLS passthrough (where the backend terminates TLS):
apiVersion: gateway.networking.k8s.io/v1alpha2kind: TLSRoutemetadata: name: mqtt-tlsspec: parentRefs: - name: production-gateway rules: - backendRefs: - name: mqtt-broker port: 8883No more Service NodePort hacks or running sidecar load balancers.
Controller Support in 2026
Gateway API is no longer bleeding edge — multiple controllers ship full or partial support:
| Controller | Status | HTTP | TCP | TLS | gRPC | Notes | |---|---|---|---|---|---| | Envoy Gateway | GA (0.6+) | ✓ | ✓ | ✓ | ✓ | Built on Envoy; Cilium uses it internally | | Cilium | GA (1.13+) | ✓ | ✓ | ✓ | ✓ | Part of eBPF dataplane; fastest | | Traefik | GA (2.10+) | ✓ | ✓ | ✓ | ✓ | Familiar Traefik + structured config | | NGINX Ingress Controller | Beta (0.6+) | ✓ | ✓ | ✓ | ✗ | Migrating from Ingress annotations | | Istio | Alpha/Beta | ✓ | ✓ | ✓ | ✓ | Coexists with Istio Gateway; overlapping models | | HAProxy | Partial | ✓ | ✓ | ✗ | ✗ | Not primary focus |
If you’re on k3s with Traefik, you can flip to Gateway API right now. On EKS with AWS Load Balancer Controller? You’ll wait — AWS is slower to adopt.
Cilium as a Reference Implementation
Cilium’s adoption is the real story. They stripped out traditional kube-proxy entirely, replaced it with eBPF, and Gateway API is their native routing model. If you’re using Cilium for CNI, Gateway API isn’t a future — it’s what you’re already running.
Gateway API Inference Extension (Brief Mention)
For advanced use cases, you can attach behavior to routes dynamically. The http.BackendRef.Filters field lets you declare transformations:
apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutespec: rules: - backendRefs: - name: api-service port: 8080 filters: - type: RequestHeaderModifier requestHeaderModifier: set: - name: X-Forwarded-By value: gateway-api - type: RequestMirror requestMirror: backendRef: name: debug-service port: 8080 percent: 10It’s extensible: custom filters can be added by the controller without changing the API.
When Ingress Is Still the Right Call
Let’s be honest: Gateway API isn’t free. It’s more resources to manage, more RBAC to think about, and you need a controller that’s fully stable.
Stick with Ingress if:
- You’re running a single-app homelab and Ingress does what you need (50% of homelabbers)
- Your cluster is on AWS and you’re using AWS Load Balancer Controller (not GA for Gateway API yet)
- You have zero multi-tenancy concerns and one person manages all routing
- Your controller (HAProxy, ancient Traefik) doesn’t support Gateway API yet
- You’re migrating to K8s and need to ship now, not design routing perfectly
Jump to Gateway API if:
- You have 3+ teams sharing the same cluster (ops owns Gateway, teams own HTTPRoutes)
- You need L4 routing, traffic splitting, or complex header matching
- You’re already using Cilium, Envoy Gateway, or modern Traefik
- You want escape hatches (ReferenceGrant, extension points) instead of annotation hacks
- Your cluster is a long-term investment, not temporary
Decision Matrix
| Scenario | Recommendation | Why |
|---|---|---|
| Single dev, single app | Ingress | No complexity tax |
| Multi-team, shared cluster | Gateway API | Role-based RBAC is game-changing |
| Canary/traffic splitting needed | Gateway API | Native semantics, no service mesh required |
| AWS EKS today | Ingress (for now) | AWS controller still Ingress-focused |
| k3s + Cilium | Gateway API | Already running it; native support |
| Running HAProxy Ingress | Ingress (wait) | Controller not there yet |
| Postgres/Redis on K8s | Gateway API | TCPRoute, not NodePort hacks |
| Massive annotation config | Gateway API | Especially if you hit 50+ annotations per Ingress |
Migration Path (If You Decide to Jump)
- Install a Gateway API controller — if you already run Traefik/Cilium/Envoy, they likely support it
- Provision a Gateway — ops/platform team creates it once in a shared namespace
- Create HTTPRoutes alongside existing Ingresses — run both for a few weeks
- Validate traffic, then delete Ingress resources — once confident, retire the old stuff
No downtime if you do this carefully.
The Real Take
Ingress solved a problem in 2015. It worked. But it hit its design ceiling around 2018, and the Kubernetes community spent years nailing annotations onto a flawed foundation instead of redesigning.
Gateway API is what Ingress should have been: composable, role-oriented, annotation-free, and built to grow. It’s not perfect (Istio and Cilium still have overlapping models; some controllers are still catching up), but it’s right.
If you’re starting a new cluster today, Gateway API is the obvious choice. If you’re running a homelab with one app and Ingress is working, nobody’s coming to yell at you for staying put. But if you’re managing multi-team routing, dealing with traffic splitting, or tired of annotation soup, the migration window is open.
Your 2 AM self will thank you for not having to hunt through 30 controller-specific annotations at 3 AM on a Friday.