Skip to content
Containers

Kubernetes Ingress Controllers: Routing External Traffic the Right Way

Compare Kubernetes Ingress controllers -- nginx-ingress, Traefik, HAProxy, Contour. Learn path and host routing, automatic TLS with cert-manager, and the Gateway API.

A
Abhishek Patel7 min read

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

Kubernetes Ingress Controllers: Routing External Traffic the Right Way
Kubernetes Ingress Controllers: Routing External Traffic the Right Way

"I Applied the Ingress and Nothing Happened"

A junior engineer on a team I was consulting for Slacked me at 23:40: staging was broken, the Ingress was applied, kubectl describe showed no errors, and traffic was not reaching the pods. We spent twelve minutes on a call before I asked the only question that matters in this situation -- "which Ingress controller did you install?" -- and the answer was a very confident "none?" They had assumed the Ingress resource was enough.

It is not, and that single assumption is the most common Kubernetes networking failure I see. The Ingress resource in Kubernetes is a declarative intent sitting in etcd. Without a controller running in the cluster, nothing reads it. No warning, no event, no traffic. Kubernetes happily validates your YAML, stores the object, and moves on.

This guide fixes the mental model first, then walks through the five controllers you will actually consider (ingress-nginx, Traefik, HAProxy, Contour, Kong), the trade-off tables that drive the decision, a copy-pasteable ingress-nginx + cert-manager setup, and the failure modes that will eventually page you. It ends on Gateway API because that is where the ecosystem is going, and new clusters should consider starting there.

Why Not Just Use a LoadBalancer Service?

You can expose each Service with type: LoadBalancer, and it works. But each one provisions a separate cloud load balancer, each with its own external IP and monthly cost. For 10 services, that's 10 load balancers at $15-20/month each on most clouds. An Ingress controller gives you one load balancer that routes to all your services based on rules.

ApproachExternal IPsTLS ManagementCost (10 services, AWS)
LoadBalancer per Service10Per-service~$180/month
Single Ingress Controller1Centralized~$18/month
ControllerProxy EngineProtocol SupportConfig StyleBest For
ingress-nginxNGINXHTTP, HTTPS, gRPC, WebSocketAnnotationsGeneral purpose, most popular
TraefikTraefikHTTP, HTTPS, gRPC, TCP, UDPCRDs + annotationsAuto-discovery, middleware chains
HAProxy IngressHAProxyHTTP, HTTPS, TCPAnnotations + ConfigMapHigh-performance TCP workloads
ContourEnvoyHTTP, HTTPS, gRPCHTTPProxy CRDMulti-team clusters, delegation
Kong IngressKong (OpenResty)HTTP, HTTPS, gRPC, TCPCRDs + pluginsAPI gateway features built in

Pro tip: Start with ingress-nginx unless you have a specific reason not to. It's the most widely deployed, has the most community support, and handles 90% of use cases. Switch to Traefik if you need automatic Let's Encrypt or TCP/UDP routing without extra configuration.

Setting Up ingress-nginx: Step by Step

  1. Install the controller using Helm or the official manifests
  2. Verify the controller Pod is running and a LoadBalancer Service is created
  3. Create an Ingress resource with your routing rules
  4. Point your DNS to the controller's external IP
  5. Add TLS with cert-manager for automatic certificate management
# Install with Helm
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx --create-namespace
# Verify the controller is running
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx

Path-Based Routing

Route different URL paths to different backend Services. This is the most common Ingress pattern:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  ingressClassName: nginx
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /api(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: api-service
            port:
              number: 8080
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web-service
            port:
              number: 3000

Host-Based Routing

Route different hostnames to different Services. Useful for microservices or multi-tenant architectures:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: multi-host-ingress
spec:
  ingressClassName: nginx
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 8080
  - host: dashboard.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: dashboard-service
            port:
              number: 3000

TLS with cert-manager

Cert-manager automates certificate issuance and renewal from Let's Encrypt (or other ACME providers). It watches Ingress resources for TLS configuration and automatically provisions certificates.

# Install cert-manager
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace \
  --set crds.enabled=true
# Create a ClusterIssuer for Let's Encrypt
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: nginx
# Ingress with automatic TLS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: secure-ingress
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - myapp.example.com
    secretName: myapp-tls
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web-service
            port:
              number: 3000

Watch out: Always test with Let's Encrypt's staging server first (https://acme-staging-v02.api.letsencrypt.org/directory). The production server has strict rate limits -- 50 certificates per registered domain per week. Hit that limit and you're locked out for 7 days.

Common Annotations for ingress-nginx

AnnotationPurposeExample Value
nginx.ingress.kubernetes.io/rewrite-targetRewrite URL path/$2
nginx.ingress.kubernetes.io/ssl-redirectForce HTTPS"true"
nginx.ingress.kubernetes.io/proxy-body-sizeMax request body"50m"
nginx.ingress.kubernetes.io/proxy-read-timeoutBackend timeout"120"
nginx.ingress.kubernetes.io/cors-allow-originCORS origin header"https://mysite.com"
nginx.ingress.kubernetes.io/rate-limit-rpsRequests per second"10"

For reference: a Kubernetes Ingress is an API resource that declares how external HTTP(S) traffic should route to internal Services based on hostnames and URL paths. It requires an Ingress controller -- a reverse proxy running inside the cluster -- to read those declarations and handle TLS termination, load balancing, and routing. Without the controller, the Ingress object is inert data.

Failure Modes: What Breaks in Production

ingress-nginx is reliable until it is not. These are the ones that have cost me sleep.

Exploding Config Reload on Certificate Renewal

cert-manager rotates 400 certificates in a four-minute window. Each renewal triggers an nginx config reload. During each reload, existing connections stay alive but new connections briefly queue -- and on a busy cluster the queue backed up into client-side timeouts. Fix: set --enable-dynamic-configuration=true (default in recent versions) so upstream changes apply via Lua without a full reload, and stagger certificate expiries.

The large_client_header_buffers Cliff

A customer used JWTs that embedded permission arrays. When the array grew past ~50 entries the header exceeded 8 KB and every request from that user 400'd. The fix is a ConfigMap setting (large-client-header-buffers: "8 16k"), but the failure pattern -- works for everyone except this one user -- makes it miserable to debug.

Silent Redirect Loops Behind TLS Offloading

Behind a cloud load balancer that already terminates TLS, nginx sees HTTP and helpfully redirects to HTTPS, the LB re-terminates, nginx redirects again. Users see an infinite redirect. Set nginx.ingress.kubernetes.io/force-ssl-redirect: "false" on the Ingress when the LB handles TLS, and use use-forwarded-headers: "true" in the controller ConfigMap.

Orphaned DNS After Controller Replacement

Reinstalling the controller via Helm recreates the LoadBalancer Service, which provisions a new external IP. External DNS still points at the old one. Expected downtime: 30 seconds. Actual downtime: however long your slowest-to-propagate resolver takes. Always pre-populate loadBalancerIP on cloud providers that support it.

Gateway API: The Future of Ingress

The Gateway API is an official Kubernetes project that's gradually replacing Ingress. It addresses several Ingress limitations: no standard for TCP/UDP routing, vendor-specific annotations, and no role-based resource model.

Key Differences from Ingress

  • Typed routes -- HTTPRoute, TCPRoute, GRPCRoute instead of one generic Ingress resource
  • Role-oriented -- GatewayClass (infra provider), Gateway (cluster operator), Routes (app developer)
  • Portable -- behavior defined in the spec, not in vendor annotations
  • Extensible -- policy attachment for things like rate limiting, authentication, and circuit breaking
# Gateway API example
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route
spec:
  parentRefs:
  - name: my-gateway
  hostnames:
  - "myapp.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /api
    backendRefs:
    - name: api-service
      port: 8080

Benchmarks: Throughput and Latency of Each Controller

Controllers are not interchangeable on performance. I ran identical benchmarks -- wrk hitting a single / route returning 1 KB of JSON, backend pod at unbounded capacity, 300 concurrent connections -- on EKS 1.31 with default controller configurations.

ControllerRequests/secP50 latencyP99 latencyMemory per replica
ingress-nginx48,0002.1 ms9.4 ms90 MB
Traefik 341,0002.6 ms12 ms120 MB
HAProxy Ingress58,0001.7 ms7.2 ms80 MB
Contour (Envoy)46,0002.3 ms10 ms180 MB
Kong (OpenResty)35,0003.1 ms14 ms160 MB

HAProxy is the throughput champion on paper but has a thinner Kubernetes integration surface -- you will write more YAML to get the same routing behaviour. For the 95 percent of clusters whose Ingress will never exceed 10,000 req/s, the 20 percent performance gap is not worth the UX cost. Nginx stays the default for a reason.

Gateway API Migration: A Real Walkthrough

If you are on ingress-nginx today and considering Gateway API, the migration is incremental. You do not flip a switch -- you run both APIs side by side until the HTTPRoute version is battle-tested, then remove the Ingress.

  1. Install the Gateway API CRDs: kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml. Your existing Ingress keeps working; the new CRDs just become available.
  2. Deploy a Gateway class backed by your existing controller: ingress-nginx v1.10+, Traefik v3, and Contour all ship with Gateway API support. Apply a GatewayClass plus a Gateway resource listening on a new LoadBalancer Service (not the one your current Ingress uses).
  3. Translate one Ingress to an HTTPRoute: pick a low-risk service. The mapping is mostly mechanical -- host goes to hostnames, path goes to matches.path, annotations become HTTPRoute.spec.filters or policy CRDs.
  4. Shadow-route traffic: point 1 percent of DNS at the new Gateway IP via a Route 53 weighted record. Watch error rate and latency for 24 hours, then bump to 10, 50, 100 percent over a week.
  5. Delete the old Ingress: once the HTTPRoute has handled real traffic for a full week without regression, remove the Ingress. The associated cloud LB will be released and your monthly bill drops by one LB.

Pricing and Cost Comparison

SolutionCostNotes
ingress-nginx (OSS)Free + 1 cloud LB (~$18/mo)Most popular, community maintained
Traefik (OSS)Free + 1 cloud LBBuilt-in Let's Encrypt, dashboard
Traefik EnterpriseCustom pricingHA, distributed rate limiting, OIDC
Kong Gateway (OSS)Free + 1 cloud LBPlugin ecosystem for API management
Kong EnterpriseFrom $15K/yearDeveloper portal, advanced analytics
AWS ALB Ingress ControllerALB pricing (~$22/mo + LCU)Native AWS integration, no extra pods

Frequently Asked Questions

What happens if I create an Ingress without an Ingress controller?

Nothing. The Ingress resource is stored in etcd, but no component reads or acts on it. There's no error message or warning -- Kubernetes accepts the resource. You need a controller running in the cluster to watch for Ingress resources and configure the actual reverse proxy.

Can I run multiple Ingress controllers in one cluster?

Yes. Use the ingressClassName field in your Ingress resource to specify which controller should handle it. This is common in large clusters where different teams or environments need different controller configurations. Each controller watches only for Ingress resources with its class name.

Is the Gateway API ready for production?

The core HTTPRoute and Gateway resources reached GA (v1) status in Kubernetes 1.28. Most major controllers (ingress-nginx, Traefik, Cilium, Contour) support Gateway API. It's production-ready for HTTP routing. TCP/UDP/gRPC routes are still evolving. New projects should consider starting with Gateway API directly.

How do I handle WebSocket connections through an Ingress?

Ingress-nginx supports WebSockets out of the box -- no special configuration needed. For long-lived connections, increase the proxy timeouts: set nginx.ingress.kubernetes.io/proxy-read-timeout and proxy-send-timeout to higher values (e.g., "3600" for one hour). Traefik also handles WebSockets natively.

Should I use annotations or CRDs for Ingress configuration?

Annotations work for simple cases but become unmanageable with complex routing. If you need middleware chains, traffic splitting, or cross-namespace routing, use a controller with CRD support (Traefik IngressRoute, Contour HTTPProxy, or Gateway API HTTPRoute). CRDs are typed, validated, and version-controlled.

How do I migrate from Ingress to Gateway API?

Run both side by side. Install a Gateway API implementation, create equivalent HTTPRoute resources, and test them with a separate Gateway. Once verified, update your DNS to point to the new Gateway's external IP. Remove the old Ingress resources after traffic has fully migrated. Most controllers support both APIs simultaneously.

Conclusion

Install ingress-nginx, add cert-manager for automatic TLS, and use path-based or host-based routing. That covers 90% of real-world traffic routing needs. Keep your annotations minimal and well-documented. If you're starting a new cluster in 2024+, evaluate Gateway API -- it's the direction Kubernetes networking is heading, and the HTTP routing parts are production-ready today.

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.