Kubernetes Gateway API vs Ingress: Why You Should Migrate
The Gateway API is the official successor to Kubernetes Ingress. Compare routing features, controller support from NGINX to Istio, and follow a practical migration guide from Ingress to HTTPRoute.
Infrastructure engineer with 10+ years building production systems on AWS, GCP,…

A Ten-Year Detour Through Annotations
The Ingress API landed in Kubernetes 1.1 in November 2015. It was deliberately minimal: hostname, path, service. The spec authors expected controllers to extend it, and they did -- aggressively, incompatibly, and with no coordination. By 2018 every production Ingress manifest was half YAML and half a string of controller-specific annotations like nginx.ingress.kubernetes.io/rewrite-target that no other controller understood. The API shape was generic; the behaviour was hard-wired to one vendor.
The community noticed. In 2019 SIG-Network kicked off a new API originally nicknamed "Service API", later renamed Gateway API. The alpha landed in 2020. TCPRoute and TLSRoute followed. GRPCRoute arrived in 2023. Core HTTP routing hit GA in Kubernetes 1.28 in August 2023, and by mid-2024 every major controller -- NGINX, Envoy, Istio, Cilium, Traefik, Kong -- shipped a GA Gateway API implementation. In 1.30 the project announced that Ingress would receive no new features. Ever.
That is the end of the story for Ingress: not a deprecation, just a freeze. The API still works, but every new feature from 2024 onwards lives in Gateway API. If you are writing Ingress manifests today, you are writing migration debt by the yard. This guide is the playbook for getting off that road.
Why the Annotation Model Broke Down
Ingress was intentionally minimal. The designers expected controllers to extend it through annotations. That decision created a mess:
- No protocol diversity -- Ingress only handles HTTP and HTTPS. Need TCP routing for a database? UDP for DNS or gaming servers? You're on your own with CRDs or NodePort hacks.
- Annotation sprawl -- Rate limiting, CORS, timeouts, rewrites, authentication -- all shoved into annotations with no validation, no documentation in the schema, and no portability between controllers.
- No role separation -- A single Ingress resource mixes infrastructure concerns (which load balancer, which IP) with application concerns (which path goes where). Platform teams and app developers edit the same resource.
- No traffic management -- Canary deployments, traffic splitting, request mirroring, and header-based routing all require controller-specific CRDs or annotations.
- Limited TLS options -- TLS passthrough requires annotations. Client certificate validation is inconsistent. There's no standard for mTLS configuration.
- Schema escape hatches go untyped -- Annotations are stringly-typed.
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"is valid;"sixty"is also accepted by the API server and fails silently in the controller. No CRD validation, no versioning.
For the record: The Gateway API is a set of CRDs -- GatewayClass, Gateway, HTTPRoute, TCPRoute, GRPCRoute, and friends -- that model routing behaviour in typed spec fields rather than vendor annotations. Same mental model as Ingress, but every behaviour is validated, versioned, and portable.
Gateway API Architecture: Three Resources, Three Roles
The Gateway API's biggest design win is separating concerns into distinct resources owned by different personas:
| Resource | Owner | Responsibility |
|---|---|---|
| GatewayClass | Infrastructure provider | Defines the controller implementation (like StorageClass for storage) |
| Gateway | Cluster operator / platform team | Configures listeners, ports, TLS settings, and allowed routes |
| HTTPRoute / TCPRoute / GRPCRoute | Application developer | Defines routing rules, backends, filters, and traffic policies |
This separation means a platform team can provision a Gateway with TLS termination, IP addresses, and security policies -- and app developers can attach routes to it without touching infrastructure config. In large organizations, this maps cleanly to existing RBAC boundaries.
# 1. Infrastructure provider installs the GatewayClass
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: nginx
spec:
controllerName: gateway.nginx.org/nginx-gateway-controller
# 2. Platform team creates the Gateway
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: production-gateway
namespace: infra
spec:
gatewayClassName: nginx
listeners:
- name: http
protocol: HTTP
port: 80
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: wildcard-tls
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: "true"
# 3. App developer attaches an HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: store-api
namespace: store
spec:
parentRefs:
- name: production-gateway
namespace: infra
hostnames:
- "store.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /api/v2
backendRefs:
- name: store-api-v2
port: 8080
Ingress vs Gateway API: Head-to-Head
| Feature | Ingress | Gateway API |
|---|---|---|
| HTTP routing | Path and host based | Path, host, header, query param, method |
| TCP/UDP routing | Not supported | TCPRoute, UDPRoute resources |
| gRPC routing | Annotations (controller-specific) | Native GRPCRoute resource |
| Traffic splitting | Annotations or CRDs | Built-in weight-based backendRefs |
| Request mirroring | Not standardized | Native RequestMirror filter |
| Header modification | Annotations | RequestHeaderModifier filter |
| TLS passthrough | Annotations | TLSRoute with mode: Passthrough |
| Role separation | None -- one resource | GatewayClass / Gateway / Route split |
| Cross-namespace routing | Not supported | ReferenceGrant resource |
| Timeouts | Annotations | Spec-level timeouts on HTTPRoute rules |
| Configuration portability | Low (annotation-dependent) | High (behavior in spec) |
| API status | Frozen (no new features) | Active development, GA core |
Routing Scenarios Compared
Canary Deployment with Traffic Splitting
With Ingress, you'd need controller-specific annotations or a separate tool like Flagger. With Gateway API, it's built into the spec:
# Gateway API: 90/10 traffic split
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: app-canary
spec:
parentRefs:
- name: production-gateway
hostnames:
- "app.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: app-stable
port: 8080
weight: 90
- name: app-canary
port: 8080
weight: 10
Header-Based Routing
Route requests to a specific backend based on headers -- useful for A/B testing or internal previews:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: header-routing
spec:
parentRefs:
- name: production-gateway
hostnames:
- "app.example.com"
rules:
- matches:
- headers:
- name: X-Preview
value: "true"
backendRefs:
- name: app-preview
port: 8080
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: app-stable
port: 8080
Controller Support in 2026
Gateway API adoption has reached critical mass. Every major proxy and service mesh supports it:
| Controller | Gateway API Support | Maturity | Notes |
|---|---|---|---|
| NGINX Gateway Fabric | HTTPRoute, GRPCRoute | GA | Official NGINX implementation, replacing ingress-nginx for Gateway API |
| Istio | HTTPRoute, TCPRoute, GRPCRoute, TLSRoute | GA | Deepest feature coverage, auto mTLS |
| Cilium | HTTPRoute, TLSRoute, GRPCRoute | GA | eBPF-based, excellent performance, no sidecar |
| Envoy Gateway | HTTPRoute, TCPRoute, UDPRoute, GRPCRoute | GA | Reference implementation, broadest route type support |
| Traefik | HTTPRoute, TCPRoute, TLSRoute | GA | Also supports its own IngressRoute CRD |
| Kong | HTTPRoute, TCPRoute, GRPCRoute | GA | API gateway features via policy attachment |
My recommendation: For new clusters, start with Envoy Gateway if you want the broadest Gateway API coverage, or NGINX Gateway Fabric if your team already knows NGINX. If you're running a service mesh, Istio and Cilium both treat Gateway API as their primary ingress path now.
Migration Guide: Ingress to HTTPRoute
You don't need a big-bang migration. Run both APIs side by side -- every controller listed above supports Ingress and Gateway API simultaneously. Here's a step-by-step approach:
Step 1: Install a Gateway API Controller
# Example: Install NGINX Gateway Fabric with Helm
helm install ngf oci://ghcr.io/nginx/charts/nginx-gateway-fabric \
--create-namespace --namespace nginx-gateway \
--set service.type=LoadBalancer
# Verify the GatewayClass exists
kubectl get gatewayclass
# NAME CONTROLLER ACCEPTED
# nginx gateway.nginx.org/nginx-gateway-controller True
Step 2: Create a Gateway
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: main-gateway
namespace: infra
spec:
gatewayClassName: nginx
listeners:
- name: http
protocol: HTTP
port: 80
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: wildcard-cert
Step 3: Convert Ingress to HTTPRoute
Here's a typical Ingress and its equivalent HTTPRoute:
# Before: Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
nginx.ingress.kubernetes.io/rate-limit-rps: "20"
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls
rules:
- host: app.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 8080
# After: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app
spec:
parentRefs:
- name: main-gateway
namespace: infra
hostnames:
- "app.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /api
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: api-svc
port: 8080
timeouts:
request: 60s
Notice that annotations like proxy-read-timeout become spec-level timeouts, and rewrite-target becomes a URLRewrite filter. Rate limiting moves to a policy attachment -- controller-specific but structured rather than annotation-based.
Step 4: Test and Switch DNS
- Apply the HTTPRoute and verify the route is accepted:
kubectl get httproute my-app -o yaml-- checkstatus.parentsforAccepted: True. - Test the new Gateway's external IP directly with
curl --resolve. - Update DNS to point to the Gateway's IP.
- Remove old Ingress resources once traffic has migrated.
Advanced: TLS Passthrough
For services that handle their own TLS (like databases or internal PKI-dependent services), use TLSRoute with passthrough mode:
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
name: db-passthrough
spec:
parentRefs:
- name: production-gateway
sectionName: tls-passthrough
hostnames:
- "db.internal.example.com"
rules:
- backendRefs:
- name: database-service
port: 5432
The Gateway listener must be configured with tls.mode: Passthrough on the corresponding section. The proxy forwards the raw TLS connection without decrypting it.
Advanced: gRPC Routing
GRPCRoute lets you match on gRPC service and method names -- something Ingress could never do natively:
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
name: grpc-routing
spec:
parentRefs:
- name: production-gateway
hostnames:
- "grpc.example.com"
rules:
- matches:
- method:
service: shop.ProductService
method: GetProduct
backendRefs:
- name: product-svc
port: 50051
- matches:
- method:
service: shop.OrderService
backendRefs:
- name: order-svc
port: 50051
Advanced: Request Mirroring
Mirror production traffic to a shadow service for testing -- without affecting responses to the client:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: mirror-traffic
spec:
parentRefs:
- name: production-gateway
hostnames:
- "app.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /api
filters:
- type: RequestMirror
requestMirror:
backendRef:
name: shadow-api
port: 8080
backendRefs:
- name: production-api
port: 8080
Performance Benchmarks
Gateway API itself doesn't add overhead -- it's a configuration API, not a data plane. Performance depends on the underlying proxy. Here's what to expect with common controllers routing 1,000 RPS through basic HTTP routing rules:
| Controller | p50 Latency | p99 Latency | Memory (idle) | Memory (1K RPS) |
|---|---|---|---|---|
| NGINX Gateway Fabric | 0.8ms | 2.1ms | ~45MB | ~80MB |
| Envoy Gateway | 0.9ms | 2.4ms | ~60MB | ~110MB |
| Cilium | 0.5ms | 1.6ms | ~120MB | ~150MB |
| Istio (ambient) | 0.7ms | 2.0ms | ~80MB | ~130MB |
| Traefik | 1.0ms | 2.8ms | ~50MB | ~95MB |
Cilium's eBPF-based data plane gives it the lowest latency since it bypasses parts of the kernel networking stack. NGINX Gateway Fabric is the most memory-efficient. For most workloads, the differences are negligible -- pick your controller based on features and ecosystem fit, not benchmarks.
Production Failure Modes and How to Catch Them
Gateway API is more robust than Ingress, but the operational muscle memory is still being built. These are the issues I have seen in the first year of running Gateway API in production across three clusters.
ReferenceGrant Drift
Cross-namespace routing requires a ReferenceGrant in the backend's namespace authorising the Gateway to reference a Service there. Delete or mis-scope the grant and the HTTPRoute quietly reports ResolvedRefs: False. Traffic 404s. Nothing crashes. Alert on gateway_api_httproute_conditions{type="ResolvedRefs",status="False"} -- you will not see this in your 5xx dashboards because the condition is surfaced on the route, not the pod.
Listener Port Conflicts
A Gateway can declare multiple listeners, but if two listeners on the same IP conflict (for example, two HTTPS on port 443 with overlapping SNI), the controller will accept one and mark the other Conflicted. Config still applies, some traffic still works -- enough to look healthy in shallow probes. Always inspect kubectl get gateway -o yaml under status.listeners[].conditions after any Gateway change.
TLS Secret Namespace Mismatches
Unlike Ingress, Gateway's certificateRefs resolves the Secret in the Gateway's namespace, not the route's. Teams migrating from Ingress routinely put the cert in the app namespace and point certificateRefs at it, which silently falls back to the default cert. Your browser shows a red padlock; your controller logs are clean. Put certs in the Gateway namespace and use cert-manager's spec.secretTemplate.namespace or a sync tool like kubernetes-reflector.
Controller Version Skew
The Gateway API CRDs ship separately from controllers. Installing CRD v1.2.0 alongside an NGINX Gateway Fabric expecting v1.1.0 can cause the controller to ignore new fields (timeouts.request, sessionPersistence) without error. Pin CRD versions in your Flux/Argo bootstrap, and run kubectl get crd gateways.gateway.networking.k8s.io -o jsonpath='{.spec.versions[*].name}' after upgrades.
PolicyAttachment Ordering Ambiguity
Policies (BackendTLSPolicy, BackendLBPolicy, and controller-specific rate-limit policies) can attach at Gateway, Route, or Service level. When multiple policies apply, the resolution order is spec'd but subtle. Two rate-limit policies targeting the same route with different RPS can silently "merge" in vendor-specific ways. Keep policy attachment shallow: prefer route-level over gateway-level, and never layer more than two.
Cost and Throughput at Scale
Gateway API itself adds no data-plane cost; the proxy handles the bytes. But the choice of controller and its deployment shape drives both monthly spend and p99 latency. Here is what a 10,000 RPS production ingress looks like across controllers on EKS, billed for the AWS side only.
| Controller | Replicas for 10k RPS | Instance type | Monthly compute | NLB/ALB cost | Total |
|---|---|---|---|---|---|
| NGINX Gateway Fabric | 3 | c7g.large ($0.0725/h) | $158.77 | $22 (NLB) | $180.77 |
| Envoy Gateway | 3 | c7g.large | $158.77 | $22 | $180.77 |
| Cilium (kubeProxyReplacement) | 0 (built-in) | Node DaemonSet | $0 extra | $22 | $22.00 |
| Istio ambient ztunnel | 0 (node-level) | Node DaemonSet | $0 extra | $22 | $22.00 |
| Traefik | 3 | c7g.large | $158.77 | $22 | $180.77 |
The Cilium/Istio ambient line is misleading -- the cost is not zero, it is amortised across the nodes you already pay for. But if you are running either of those meshes, adding Gateway API support costs you only a DaemonSet memory bump of roughly 50-80 MB per node. For pure ingress without a mesh, NGINX Gateway Fabric is the cheapest disciplined choice.
Operational Checklist Before You Cut Over DNS
Before flipping production DNS from the old ingress to a new Gateway, walk through this list. I have forgotten one or more of these during every migration I have done.
- Verify HTTPRoute is Accepted --
kubectl get httproute -A -o json | jq '.items[] | select(.status.parents[0].conditions[] | select(.type=="Accepted" and .status!="True"))'. If anything prints, fix it first. - Test with Host header, not DNS --
curl --resolve app.example.com:443:<gateway-ip> https://app.example.com/healthz. Exercise every critical path. - Confirm TLS chain --
openssl s_client -connect <gateway-ip>:443 -servername app.example.com -showcerts. Check that intermediate certs are present; some controllers omit them by default. - Warm the cache if any -- If you are behind a CDN, pre-fetch top URLs to populate the new origin. Cold caches during DNS cutover amplify origin load.
- Lower DNS TTL 24 hours in advance -- Drop to 60 seconds. This is the boring step that makes rollback fast.
- Watch 5xx and TLS handshake rate for 10 minutes after cutover -- Cilium in particular has a one-time initial connection spike as eBPF maps populate.
- Keep the old Ingress for 48 hours -- Do not delete it until logs confirm no clients are still hitting the old LB.
Frequently Asked Questions
Is the Ingress API being deprecated?
Not formally removed, but it's frozen. The Kubernetes project has stated that no new features will be added to the Ingress resource. It will remain in the API for backward compatibility, but all new networking features land exclusively in Gateway API. Starting new projects on Ingress means you'll miss out on traffic splitting, gRPC routing, request mirroring, and everything else that's been built since 2023.
Can I run Gateway API and Ingress side by side?
Yes, and this is the recommended migration path. Every major controller supports both APIs simultaneously. You can migrate routes one at a time, test each HTTPRoute against the Gateway's external IP, and switch DNS when you're confident. There's no reason to do a big-bang cutover.
Which Gateway API controller should I choose?
It depends on your stack. If you're already running Istio, use its built-in Gateway API support. If you want an eBPF-based solution with no sidecars, go with Cilium. For the broadest route type coverage and the closest alignment to the spec, Envoy Gateway is the reference implementation. For teams comfortable with NGINX, NGINX Gateway Fabric is the natural choice. All of them are production-ready.
Do I need to install Gateway API CRDs separately?
Usually yes. The Gateway API CRDs aren't bundled with Kubernetes itself. Most controller Helm charts offer an option to install them automatically, but you can also install them manually with kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml. Check your controller's docs for the supported version.
How do timeouts work in Gateway API vs Ingress?
In Ingress, timeouts are set via controller-specific annotations like nginx.ingress.kubernetes.io/proxy-read-timeout. In Gateway API, timeouts are part of the HTTPRoute spec: you set timeouts.request (total request duration) and timeouts.backendRequest (time waiting for the backend) directly on a rule. They're portable and validated by the API server.
What about rate limiting and authentication?
Gateway API uses a "policy attachment" model for cross-cutting concerns. Rate limiting, authentication, and circuit breaking aren't in the core spec -- instead, controllers define policy resources that attach to Gateways or Routes. This is intentional: these features are inherently implementation-specific. The benefit over Ingress annotations is that policies are structured, validated, and versionable rather than opaque strings.
Is Gateway API only for north-south traffic?
No. While its primary use case is north-south (external to cluster) traffic, service meshes like Istio and Cilium also use Gateway API resources for east-west (service-to-service) routing. Istio's ambient mode, for example, uses HTTPRoute to configure L7 traffic policies between services without sidecars. The GAMMA (Gateway API for Mesh Management and Administration) initiative is standardizing this.
Conclusion
The Gateway API isn't just "Ingress but better" -- it's a fundamentally different model for service networking. The role separation alone justifies migration in any team larger than one person. Add native traffic splitting, gRPC routing, request mirroring, and spec-level timeouts, and there's no technical reason to stick with Ingress for new work. Install your preferred controller, create a Gateway, and start converting Ingress resources to HTTPRoutes one at a time. Your future self will thank you when you need to do a canary deployment and it's three lines of YAML instead of a prayer and an annotation.
Written by
Abhishek Patel
Infrastructure engineer with 10+ years building production systems on AWS, GCP, and bare metal. Writes practical guides on cloud architecture, containers, networking, and Linux for developers who want to understand how things actually work under the hood.
Related Articles
Multi-Cluster Kubernetes: Argo CD ApplicationSet Patterns
When 10+ clusters or 50+ services break hand-written GitOps. ApplicationSet's four generators (cluster list, Git directory, PR, cluster decision), real production patterns (env promotion, per-tenant, multi-region failover, preview envs), and the sharp edges (template debugging, cascading mistakes, RBAC).
11 min read
ContainersKubernetes GPU Scheduling: DRA, KAI Scheduler, MIG
Dynamic Resource Allocation replaced device plugins for GPU claims in Kubernetes 1.34. KAI Scheduler adds gang scheduling and queues. MIG slices H100s into 7 isolated tenants. Full production setup with the NVIDIA GPU Operator, topology-aware training, and when to use MIG vs MPS vs time-slicing.
17 min read
CI/CDProgressive Delivery with Argo Rollouts: Canary + Analysis
Argo Rollouts replaces Kubernetes Deployments with a CRD that does weighted canary, metric-gated analysis, and automatic rollback. Production recipe, Prometheus AnalysisTemplates, and a side-by-side with Flagger.
15 min read
Enjoyed this article?
Get more like this in your inbox. No spam, unsubscribe anytime.