Skip to content
Go back

Gateway API vs Ingress in 2026

By SumGuy 11 min read
Gateway API vs Ingress in 2026

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:

…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:

classic-nginx-ingress-with-annotations.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
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: 8080

That’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.”

gatewayclass.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: cilium-gateway
spec:
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.

gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: production-gateway
namespace: ingress-cilium
spec:
gatewayClassName: cilium-gateway
listeners:
- name: http
protocol: HTTP
port: 80
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: production-cert
kind: Secret

This 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:

httproute.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-routes
namespace: default
spec:
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: 8080

No annotations. No controller-specific hacks. Just spec.

Side-by-Side: Ingress vs HTTPRoute

Same routing logic, two approaches. Ingress lives in the ingress namespace:

ingress-approach.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
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: 8080

HTTPRoute lives with the application (same namespace as the Service):

gateway-api-approach.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api
namespace: default # Alongside the app
spec:
parentRefs:
- name: production-gateway
namespace: ingress-cilium
hostnames:
- "api.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: api-service
port: 8080

TLS 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:

canary-split.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-canary
namespace: default
spec:
parentRefs:
- name: production-gateway
hostnames:
- "api.example.com"
rules:
- backendRefs:
- name: api-stable
port: 8080
weight: 90
- name: api-canary
port: 8080
weight: 10

Ingress? 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):

header-based-routing.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-debug
namespace: default
spec:
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: 8080

Again: 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:

reference-grant.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-api-routes
namespace: databases # The namespace being referenced
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: default # Only HTTPRoutes in 'default' namespace
to:
- group: ""
kind: Service
name: postgres-primary

Now 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:

tcp-route.yaml
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TCPRoute
metadata:
name: postgres
namespace: databases
spec:
parentRefs:
- name: production-gateway
rules:
- backendRefs:
- name: postgres-primary
port: 5432

And TLSRoute for TLS passthrough (where the backend terminates TLS):

tls-route.yaml
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
name: mqtt-tls
spec:
parentRefs:
- name: production-gateway
rules:
- backendRefs:
- name: mqtt-broker
port: 8883

No 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:

backend-filters.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
spec:
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: 10

It’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:

Jump to Gateway API if:

Decision Matrix

ScenarioRecommendationWhy
Single dev, single appIngressNo complexity tax
Multi-team, shared clusterGateway APIRole-based RBAC is game-changing
Canary/traffic splitting neededGateway APINative semantics, no service mesh required
AWS EKS todayIngress (for now)AWS controller still Ingress-focused
k3s + CiliumGateway APIAlready running it; native support
Running HAProxy IngressIngress (wait)Controller not there yet
Postgres/Redis on K8sGateway APITCPRoute, not NodePort hacks
Massive annotation configGateway APIEspecially if you hit 50+ annotations per Ingress

Migration Path (If You Decide to Jump)

  1. Install a Gateway API controller — if you already run Traefik/Cilium/Envoy, they likely support it
  2. Provision a Gateway — ops/platform team creates it once in a shared namespace
  3. Create HTTPRoutes alongside existing Ingresses — run both for a few weeks
  4. 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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Longhorn vs Rook-Ceph
Next Post
Rock 5B vs Orange Pi 5 vs Pi 5

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts