Skip to content
Containers

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.

A
Abhishek Patel15 min read

Infrastructure engineer with 10+ years building production systems on AWS, GCP,…

Kubernetes Gateway API vs Ingress: Why You Should Migrate
Kubernetes Gateway API vs Ingress: Why You Should Migrate

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:

ResourceOwnerResponsibility
GatewayClassInfrastructure providerDefines the controller implementation (like StorageClass for storage)
GatewayCluster operator / platform teamConfigures listeners, ports, TLS settings, and allowed routes
HTTPRoute / TCPRoute / GRPCRouteApplication developerDefines 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

FeatureIngressGateway API
HTTP routingPath and host basedPath, host, header, query param, method
TCP/UDP routingNot supportedTCPRoute, UDPRoute resources
gRPC routingAnnotations (controller-specific)Native GRPCRoute resource
Traffic splittingAnnotations or CRDsBuilt-in weight-based backendRefs
Request mirroringNot standardizedNative RequestMirror filter
Header modificationAnnotationsRequestHeaderModifier filter
TLS passthroughAnnotationsTLSRoute with mode: Passthrough
Role separationNone -- one resourceGatewayClass / Gateway / Route split
Cross-namespace routingNot supportedReferenceGrant resource
TimeoutsAnnotationsSpec-level timeouts on HTTPRoute rules
Configuration portabilityLow (annotation-dependent)High (behavior in spec)
API statusFrozen (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:

ControllerGateway API SupportMaturityNotes
NGINX Gateway FabricHTTPRoute, GRPCRouteGAOfficial NGINX implementation, replacing ingress-nginx for Gateway API
IstioHTTPRoute, TCPRoute, GRPCRoute, TLSRouteGADeepest feature coverage, auto mTLS
CiliumHTTPRoute, TLSRoute, GRPCRouteGAeBPF-based, excellent performance, no sidecar
Envoy GatewayHTTPRoute, TCPRoute, UDPRoute, GRPCRouteGAReference implementation, broadest route type support
TraefikHTTPRoute, TCPRoute, TLSRouteGAAlso supports its own IngressRoute CRD
KongHTTPRoute, TCPRoute, GRPCRouteGAAPI 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

  1. Apply the HTTPRoute and verify the route is accepted: kubectl get httproute my-app -o yaml -- check status.parents for Accepted: True.
  2. Test the new Gateway's external IP directly with curl --resolve.
  3. Update DNS to point to the Gateway's IP.
  4. 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:

Controllerp50 Latencyp99 LatencyMemory (idle)Memory (1K RPS)
NGINX Gateway Fabric0.8ms2.1ms~45MB~80MB
Envoy Gateway0.9ms2.4ms~60MB~110MB
Cilium0.5ms1.6ms~120MB~150MB
Istio (ambient)0.7ms2.0ms~80MB~130MB
Traefik1.0ms2.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.

ControllerReplicas for 10k RPSInstance typeMonthly computeNLB/ALB costTotal
NGINX Gateway Fabric3c7g.large ($0.0725/h)$158.77$22 (NLB)$180.77
Envoy Gateway3c7g.large$158.77$22$180.77
Cilium (kubeProxyReplacement)0 (built-in)Node DaemonSet$0 extra$22$22.00
Istio ambient ztunnel0 (node-level)Node DaemonSet$0 extra$22$22.00
Traefik3c7g.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.

  1. 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.
  2. Test with Host header, not DNS -- curl --resolve app.example.com:443:<gateway-ip> https://app.example.com/healthz. Exercise every critical path.
  3. 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.
  4. 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.
  5. Lower DNS TTL 24 hours in advance -- Drop to 60 seconds. This is the boring step that makes rollback fast.
  6. 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.
  7. 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.

A

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

Enjoyed this article?

Get more like this in your inbox. No spam, unsubscribe anytime.

Comments

Loading comments...

Leave a comment

Stay in the loop

New articles delivered to your inbox. No spam.