← All writing

Kubernetes Ingress Controllers Compared: Nginx vs. Traefik vs. Envoy & The Rise of the Gateway API

· Sourabh G Kulkarni

Exposing cluster traffic was supposed to be simple. Instead, teams drowned in custom annotations, reload latency, and fragmented controllers. Here is an architectural deep dive into Nginx, Traefik, Envoy-based proxies, and why the new Gateway API is replacing Ingress altogether.

Kubernetes Ingress Controllers Compared: Nginx vs. Traefik vs. Envoy & The Rise of the Gateway API

When teams deploy their first containerized application to Kubernetes, routing traffic into the cluster seems deceptively straightforward. You spin up a Deployment, declare a Service, and configure an ingress rule.

Then reality hits production scale.

You discover that basic routing is only 10% of the battle. You need SSL/TLS termination with automatic certificate renewal, canary deployments with weighted traffic splitting, WebSocket connection multiplexing, gRPC streaming, rate limiting, mutual TLS (mTLS), and header-based rewrites.

Soon, your standard Kubernetes Ingress YAML becomes an unreadable nightmare of 35 vendor-specific annotations:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "20"
    nginx.ingress.kubernetes.io/configuration-snippet: |
      more_set_headers "X-Engine-Cluster: prod-k8s-us-east";

Why did exposing HTTP traffic in Kubernetes become so complex? How do the top reverse proxies—NGINX, Traefik, and Envoy—actually handle traffic under the hood? And why did the Kubernetes SIG Network spend years redesigning this from scratch with the Kubernetes Gateway API?

Let’s unpack the architectural reality.


1. The Anatomy of an Ingress Controller

Before comparing tools, we must demystify what an Ingress Controller actually is.

Kubernetes Ingress & Gateway API ArchitectureKubernetes Ingress & Gateway API Architecture Figure 1: Client traffic flow from external edge load balancers through reverse proxy engines and dynamic service endpoints.

In Kubernetes, an Ingress resource is just metadata in etcd. By itself, it does nothing. Creating an Ingress object creates no listeners, opens no ports, and routes no packets.

To make it work, you must run an Ingress Controller, which is composed of two distinct components running inside the same pod:

┌─────────────────────────────────────────────────────────────────────────────┐
│                          Ingress Controller Pod                             │
├─────────────────────────────────────┬───────────────────────────────────────┤
│ 1. Control Plane (K8s Watcher)      │ 2. Data Plane (Reverse Proxy)         │
│ - Long-polling the K8s API server   │ - NGINX, Traefik, or Envoy            │
│ - Watches: Ingresses, Services,     │ - Accepts external client TCP sockets │
│   Endpoints / EndpointSlices        │ - Terminates TLS (reads Secrets)      │
│ - Translates K8s specs into proxy   │ - Load balances directly to Pod IPs   │
│   routing tables                    │                                       │
└─────────────────────────────────────┴───────────────────────────────────────┘
                                  │
                                  ▼
               Pod IPs (Bypassing kube-proxy / iptables)
  1. The Control Plane (The Go Operator / Reconciler): A daemon written in Go that establishes a persistent watch on the Kubernetes API server. Whenever a developer creates an Ingress, scales a Deployment, or a Pod terminates, the controller catches the event, gathers the new Pod IP addresses from EndpointSlices, and translates them into the proxy's native configuration format.

  2. The Data Plane (The Reverse Proxy Engine): The high-performance proxy (NGINX C-workers, Traefik Go routines, or Envoy C++ threads) that actually terminates incoming client TLS connections, inspects HTTP headers, evaluates routing rules, and forwards the packets directly to the destination Pod IPs.

Crucial Architectural Detail: High-performance Ingress controllers do not route traffic through ClusterIP iptables or IPVS rules. They bypass kube-proxy entirely and proxy traffic directly to the Pod IP addresses discovered from Kubernetes EndpointSlices. This eliminates unnecessary kernel packet hops and preserves low latency.


2. Contender 1: NGINX Ingress (The Battle-Tested Workhorse)

NGINX is the default standard for Kubernetes networking. But in the Kubernetes world, there is massive confusion because two entirely different NGINX ingress controllers exist:

  1. ingress-nginx (Community / Kubernetes SIG): Maintained by the Kubernetes community (registry.k8s.io/ingress-nginx/controller). It is built on top of OpenResty (NGINX with LuaJIT).
  2. NGINX Ingress Controller (F5 / NGINX Inc): Maintained commercially by F5/NGINX (nginx/nginx-ingress), offering free OSS and paid Plus versions.

When 90% of engineers say "NGINX Ingress", they mean the community version.

Incoming Request ──► NGINX Master ──► Worker Process (Lua Shared Memory) ──► Pod IP

The Architectural Strengths

  • Decades of Battle Hardening: NGINX is rock solid. Its event-driven, asynchronous worker model handles hundreds of thousands of concurrent connections with minimal RAM and CPU overhead.
  • Deep Lua Integration: The community controller uses OpenResty Lua modules to update upstream endpoint lists dynamically in shared memory without needing to write out a new file on disk every time a single pod scales.
  • Extensive Ecosystem Support: Nearly every open-source Helm chart, tutorial, and CI/CD pipeline on Earth ships with default annotations for ingress-nginx.

The Architectural Achilles' Heel: The Configuration Reload Bottleneck

While endpoint changes (pod scaling up/down) are handled dynamically via Lua shared memory, structural changes do not bypass NGINX configuration reloads.

If you change:

  • A routing path rule (/api/v1 to /api/v2)
  • A TLS certificate Secret
  • An ingress annotation modifying buffer sizes or timeouts

The controller must regenerate /etc/nginx/nginx.conf and execute an nginx -s reload.

In an enterprise cluster with 500+ microservices where developers constantly deploy new routes, an NGINX controller can get caught in a perpetual reload loop. Under high traffic, rapid reloads cause worker processes to linger while waiting for existing connections to drain, leading to memory spikes and brief connection latency jitter.


3. Contender 2: Traefik (The Dynamic Cloud-Native Pioneer)

Created by Containous (now Traefik Labs), Traefik was built from day one specifically for dynamic, containerized environments like Docker, Swarm, and Kubernetes.

Unlike NGINX, which was a static web server retrofitted into Kubernetes, Traefik was written in Go with dynamic microservice service discovery baked into its core.

Client ──► Traefik EntryPoint ──► Middlewares (Auth, RateLimit) ──► Router ──► Service ──► Pod

The Architectural Strengths

  • True Zero-Reload Architecture: Traefik does not have a static configuration file that must be written and reloaded. When the Kubernetes API reports a route or certificate change, Traefik updates its internal in-memory routing tree on the fly without dropping connections or spawning new worker processes.
  • Built-in Automatic ACME (Let's Encrypt): Traefik natively talks to Let's Encrypt to provision and rotate TLS certificates automatically without requiring an external cert-manager daemon for simple setups.
  • Composable Middlewares: Traefik introduces clean, chainable middleware concepts. Instead of messy Lua snippets or NGINX configuration hacks, you define reusable Kubernetes Custom Resources (CRDs) for rate limiting, basic auth, retry logic, and header transformations:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: api-rate-limit
  namespace: production
spec:
  rateLimit:
    average: 100
    burst: 50

The Trade-Offs

Because Traefik is written in Go, its garbage collector (GC) and network runtime can require slightly more CPU and baseline memory under massive socket concurrency (100k+ active connections) compared to tuned C/C++ proxies like NGINX or Envoy.


4. Contender 3: Envoy-Based Controllers (The Modern Enterprise Standard)

Envoy Proxy, originally engineered by Lyft and donated to the CNCF, represents the cutting edge of modern network proxies. Written in modern C++, Envoy was engineered for massive multi-threaded performance, deep observability, and completely dynamic configuration.

Several major Kubernetes Ingress and API Gateway controllers are powered by Envoy:

  • Envoy Gateway (The official CNCF Gateway API reference implementation)
  • Contour (Originated by Heptio / VMware)
  • Emissary-Ingress / Ambassador
  • Istio Ingress Gateway
K8s API Server ──► Envoy Control Plane ──[ gRPC xDS Streaming API ]──► Envoy Proxy Workers

The Architectural Superiority: The xDS Dynamic API

Envoy does not have configuration files in the traditional sense. It exposes a set of discovery APIs known as xDS:

  • LDS (Listener Discovery Service): Dynamically updates ports and TLS certificates.
  • RDS (Route Discovery Service): Dynamically updates HTTP paths, headers, and rewrite rules.
  • CDS (Cluster Discovery Service): Dynamically updates backend upstream groups.
  • EDS (Endpoint Discovery Service): Dynamically streams live Pod IP address changes.

The Envoy Ingress controller acts as an xDS gRPC server. When you mutate an Ingress or Gateway resource, the controller streams the delta directly to the running Envoy process over gRPC. Envoy hot-applies routing, TLS, and upstream changes instantaneously in memory without dropped packets, connection draining, or process restarts.

Observability Unmatched

Envoy emits over 200 standard metrics per route, cluster, and listener natively formatted for Prometheus out of the box:

  • Exact upstream connection latency percentiles (P50, P95, P99)
  • Active connection pools, HTTP/2 multiplex streams, and gRPC error status codes
  • Native distributed tracing headers (x-request-id, OpenTelemetry, W3C TraceContext)

5. The Fatal Flaw of the Original Kubernetes Ingress Spec

Why did the Kubernetes community embark on a multi-year effort to build the Gateway API if NGINX, Traefik, and Envoy already existed?

Because the original v1.Ingress API specification, written in 2015, had three catastrophic architectural flaws:

Flaw 1: The "Lowest Common Denominator" Problem

The core Kubernetes Ingress specification only standardized three basic things:

  1. Host matching (host: api.sourabhkulkarni.in)
  2. Path matching (path: /users)
  3. A TLS secret reference (secretName: cert-prod)

Anything more advanced—traffic weighting, rate limiting, request rewrites, header injection, circuit breaking—had no standard representation in the Kubernetes API.

Every controller author was forced to invent custom, vendor-locked annotations:

  • NGINX: nginx.ingress.kubernetes.io/rewrite-target
  • Traefik: traefik.ingress.kubernetes.io/router.middlewares
  • Emissary: getambassador.io/config

Switching from NGINX to Traefik or Envoy meant rewriting every single Ingress manifest across your company.

Flaw 2: Single-Resource Multi-Tenancy Disaster

In traditional Ingress, everything is stuffed into a single monolithic file owned by a single namespace:

  • Who gets to bind the public domain sourabhkulkarni.in?
  • Who configures the company TLS certificate?
  • Who routes /checkout vs /billing?

In v1.Ingress, if an application developer makes a typo in their service path or an annotation in a staging namespace, they can inadvertently hijack the root domain or trigger a controller reload that impacts production traffic.


6. The Future Has Arrived: The Kubernetes Gateway API

The Kubernetes Gateway API is not just an evolution of Ingress; it is an entirely new networking paradigm built from the ground up to solve multi-tenancy, cross-namespace routing, and vendor neutrality.

Instead of one monolithic resource, the Gateway API separates responsibilities into three distinct personas and resources:

┌────────────────────────────────────────────────────────────────────────┐
│ Persona: Cloud / Infrastructure Architect                              │
│ Resource: GatewayClass (Defines controller: envoy-gateway, traefik)   │
└───────────────────────────────────┬────────────────────────────────────┘
                                    │
┌───────────────────────────────────▼────────────────────────────────────┐
│ Persona: Cluster / DevOps Platform Engineer                           │
│ Resource: Gateway (Binds ports 80/443, attaches wildcard TLS certs,   │
│           authorizes which namespaces are allowed to attach routes)   │
└───────────────────────────────────┬────────────────────────────────────┘
                                    │
                                    ├──────────────────────────┐
┌───────────────────────────────────▼───────────┐ ┌────────────▼─────────┐
│ Persona: Service A Developer (orders namespace)│ │ Persona: Service B   │
│ Resource: HTTPRoute                           │ │ Resource: HTTPRoute  │
│ Attaches /orders to Gateway                   │ │ Attaches /auth       │
│ Defines canary weight: 90/10                  │ │ Defines timeouts     │
└───────────────────────────────────────────────┘ └──────────────────────┘

Native Canary Splitting Without Hacks

In the Gateway API, weighted traffic splitting is a first-class citizen inside the official Kubernetes specification. No annotations, no regex tricks:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: billing-route
  namespace: billing
spec:
  parentRefs:
    - name: enterprise-gateway
      namespace: platform-infra
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api/billing
      backendRefs:
        - name: billing-v1
          port: 8080
          weight: 90
        - name: billing-v2
          port: 8080
          weight: 10

7. Architectural Comparison Matrix

Architectural FeatureCommunity NGINX (ingress-nginx)Traefik ProxyEnvoy-Based (e.g. Envoy Gateway)
Data Plane EngineNGINX + LuaJIT (C / Lua)Native Go routinesEnvoy Proxy (C++)
Config Dynamic ModelShared memory for Pod IPs; reload required for paths/TLSZero reloads; in-memory tree updatesZero reloads; live xDS streaming
Performance & LatencyUltra-low CPU/RAM; high raw throughputFast; minor GC latency at extreme scaleHighest throughput; deterministic P99
Advanced FeaturesCustom annotations / Lua snippetsNative CRDs & chainable MiddlewaresNative xDS filters, rate limit, mTLS
Automatic TLS (ACME)Requires cert-managerBuilt-in native Let's EncryptRequires cert-manager
Observability & MetricsStandard Prometheus exporterBuilt-in dashboard & metricsComprehensive Prometheus & OpenTelemetry
Gateway API ConformanceExperimental / PartialFull GA ConformanceNative / Core Architecture

8. The Architect's Decision Framework

Which controller should you choose in 2026?

                       Do you have existing Helm charts & legacy Ingress manifests?
                                              │
                      ┌───────────────────────┴────────────────────────┐
                      ▼ Yes                                            ▼ No
          Is high reload frequency                         Are you building a modern,
          causing connection jitter?                       multi-tenant internal platform?
              │                  │                                     │
       No ────┘                  └───► Yes                      Yes ───┴───►
          │                            │                                   │
          ▼                            ▼                                   ▼
┌──────────────────┐         ┌──────────────────┐               ┌───────────────────────┐
│ Community NGINX  │         │ Traefik Proxy    │               │ Envoy Gateway /       │
│ - Zero surprise  │         │ - Zero reloads   │               │ Kubernetes Gateway API│
│ - Universal docs │         │ - Clean CRDs     │               │ - Role separation     │
│ - Standard ops   │         │ - Built-in ACME  │               │ - Native canary/xDS   │
└──────────────────┘         └──────────────────┘               └───────────────────────┘

Recommendation 1: Stick with NGINX Ingress if...

You have hundreds of legacy microservices already functioning reliably, your cluster is relatively static without thousands of route updates per hour, and your operations team already knows how to debug nginx.conf and read access logs blindfolded.

Recommendation 2: Pick Traefik if...

You are running Kubernetes in edge or mid-sized enterprise environments, want native Let's Encrypt without the operational overhead of configuring cert-manager, and love clean, readable YAML CRDs with dynamic reloads.

Recommendation 3: Standardize on Envoy & Gateway API if...

You are building an Internal Developer Platform (IDP), operating large multi-tenant clusters, or modernizing your cloud infrastructure. The Kubernetes Gateway API is the official future of Kubernetes networking, and Envoy is the battle-tested data plane powering the world's most resilient distributed platforms.