Skip to content
Published on

Traefik vs ingress-nginx — What to Choose and When

Authors

Introduction

Almost every team that exposes a service from a Kubernetes cluster runs into the same first question: which ingress controller should we use. And in the search for an answer, two candidates come up for comparison more than any others — Traefik and ingress-nginx.

On the surface they do the same thing. They receive inbound HTTP/HTTPS traffic from the outside world and route it to the appropriate service inside the cluster. But dig a little deeper and the two projects differ substantially — in their starting points, their design philosophy, the way they express configuration, the mechanism they use to handle dynamic changes, and the position they occupy in the ecosystem.

I have run both controllers in production, across on-premises clusters for financial-sector customers and a cloud-based AI serving platform. In that process I learned that "which one is better" is actually the wrong question. The more accurate question is "which one fits our team's operating model and requirements better."

In 2026 there is one more important piece of context to add. The Kubernetes Ingress API is functionally frozen — no new features are being added. Its successor standard, Gateway API, has reached GA and has effectively become the next-generation standard. On top of that, the ingress-nginx project has moved into maintenance mode, and it carries a history of several significant security issues. These changes are shifting the center of gravity in controller selection.

In this article I compare the two controllers in depth, from design philosophy to practical operational detail, show side-by-side YAML for how the same requirement is implemented on each side, and finish with scenario-based recommendations and a decision table.

The Identity of Each Controller

ingress-nginx — a Thin Control Layer on Top of Proven NGINX

First, some terminology, because "nginx ingress" can refer to two different projects and causes a lot of confusion.

  • ingress-nginx: the controller maintained by the Kubernetes community project. It is the subject of this article and the most widely used option.
  • nginx-ingress: a separate commercial/open-source controller maintained by NGINX (F5). It has a different annotation scheme and CRDs.

In this article, "ingress-nginx" means the former — the community project maintained at kubernetes.github.io/ingress-nginx.

The core idea of ingress-nginx is: "use NGINX, which has been proven for decades, as the data plane, and put a thin control layer on top that watches Kubernetes resources and generates nginx.conf." When the controller detects a change in an Ingress resource, it produces a new nginx.conf through templating and tells NGINX to reload.

The advantages of this design are clear. NGINX is an industry standard for performance and stability, and operators are already familiar with it. The drawbacks are equally clear. Fine-grained behavior control comes to depend on annotations on the Ingress resource, and complex requirements tend to drift toward injecting raw nginx.conf snippets.

Traefik — a Proxy Designed for Cloud Native from the Start

Traefik is a reverse proxy written in Go, designed from the ground up for dynamic, container-centric environments. Since the Docker days, its core value proposition has been: "in an environment where services come and go, routing updates itself automatically without a human rewriting a config file."

Traefik configuration splits into static configuration and dynamic configuration. Static configuration covers things decided at process startup — EntryPoints (listening ports), providers, logging — while dynamic configuration covers routing rules that keep changing at runtime — Routers, Services, Middlewares. In Kubernetes you can express this dynamic configuration either through Ingress resources or through Traefik's own CRDs (IngressRoute, Middleware, and so on).

The key differentiator is that Traefik applies dynamic configuration without disruption. Even when routing rules change, no process reload or restart is required. This is the most fundamental design difference separating the two controllers.

Comparing Design Philosophy

If we compress the difference into a single table, it looks like this.

Aspectingress-nginxTraefik
Data planeNGINX (C-based)Native Go proxy
Starting pointK8s adapter over existing NGINXCloud-native by design
Config expressionIngress + annotations + snippetsIngress or CRD (IngressRoute/Middleware)
Config applicationGenerate nginx.conf, then reloadDisruption-free dynamic apply
Middleware modelAnnotation/snippet basedDeclarative Middleware chain
Automatic TLSSeparate cert-manager setupBuilt-in ACME (Let's Encrypt)
ObservabilityMetrics/logs (much extra setup)Built-in metrics/tracing/dashboard
Config philosophyCloser to imperativeDeclaration-oriented

This table alone reveals that the two projects approach the same problem with very different mindsets. ingress-nginx focuses on "connecting a powerful, familiar engine to Kubernetes," while Traefik focuses on "assimilating the proxy itself into Kubernetes's declarative model."

Configuration Style — Annotations vs CRD/Middleware

This is the area where the felt difference is largest in day-to-day operations.

The ingress-nginx Annotation Model

In ingress-nginx, the body of an Ingress resource expresses only simple host/path routing, while nearly every additional behavior (redirects, rewrites, auth, rate limiting, timeouts, and so on) is specified via annotations.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-app
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
    nginx.ingress.kubernetes.io/ssl-redirect: 'true'
    nginx.ingress.kubernetes.io/proxy-body-size: 50m
    nginx.ingress.kubernetes.io/rate-limit-rps: '20'
    nginx.ingress.kubernetes.io/configuration-snippet: |
      more_set_headers "X-Custom: hello";
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /web(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: web-svc
                port:
                  number: 80

This approach is very fast and intuitive in simple cases. But as requirements grow more complex, annotation keys pile up in long lists, and eventually you end up injecting raw nginx.conf fragments via configuration-snippet or server-snippet. Snippet injection is powerful but also the most dangerous part. A single malformed snippet can fail an entire reload, and in the past it has even become a path for security bypasses. In fact, the serious ingress-nginx vulnerabilities reported in 2025 were closely related to this snippet/config-injection path.

The Traefik CRD and Middleware Model

Traefik expresses additional behavior not through annotations but through Middleware — an independent, declarative resource. And routing itself can be expressed more richly through the IngressRoute CRD.

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: rate-limit
spec:
  rateLimit:
    average: 20
    burst: 40
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: custom-header
spec:
  headers:
    customRequestHeaders:
      X-Custom: hello
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: web-app
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`app.example.com`) && PathPrefix(`/web`)
      kind: Rule
      middlewares:
        - name: rate-limit
        - name: custom-header
      services:
        - name: web-svc
          port: 80

Do you see the difference. In Traefik, a policy such as rate-limit is a reusable object that multiple routes can reference, and you can even separate permissions with RBAC. Routing match rules are also written as expressions that combine Host and PathPrefix with logical operators. This is easier to validate and test than annotation strings, and it is friendlier for tracking change history in a GitOps environment.

There is a cost, of course. You must install CRDs in the cluster, and your team must learn Traefik-specific resources rather than the Ingress standard. You can use Traefik with standard Ingress resources too, but doing so forfeits a good portion of Traefik's strengths.

Automatic TLS

Issuing and renewing HTTPS certificates is an unavoidable operational topic. The two controllers approach it very differently.

Traefik's Built-in ACME

Traefik builds integration with ACME providers like Let's Encrypt directly into the controller. Define a certificate resolver in the static configuration, and it automatically issues and renews certificates for domains arriving at an EntryPoint.

# Traefik static configuration (part of values.yaml)
certificatesResolvers:
  letsencrypt:
    acme:
      email: ops@example.com
      storage: /data/acme.json
      httpChallenge:
        entryPoint: web

Specify this resolver in your IngressRoute and you are done. No separate controller installation is required.

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: secure-app
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`secure.example.com`)
      kind: Rule
      services:
        - name: secure-svc
          port: 80
  tls:
    certResolver: letsencrypt

That said, this built-in ACME often assumes single-instance storage (acme.json), so scaling out to multiple replicas requires distributed storage or external certificate management. In a high-availability setup this can become a trap.

ingress-nginx + cert-manager

ingress-nginx does not build certificate issuance into itself. Instead it pairs with cert-manager, the de facto standard. cert-manager declares certificates with the Certificate/Issuer CRDs, stores issued certificates as Secrets, and ingress-nginx references those Secrets.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ops@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
      - http01:
          ingress:
            class: nginx
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: secure-app
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - secure.example.com
      secretName: secure-app-tls
  rules:
    - host: secure.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: secure-svc
                port:
                  number: 80

At first glance it looks more complex because there is one more component, but in practice this separation tends to be an advantage. cert-manager broadly supports HA environments, wildcard certificates, the DNS-01 challenge, and integration with internal PKI (Vault, private ACME CA). Because certificate management responsibility is separated from the controller, you can keep reusing the certificate infrastructure even if you swap controllers.

Performance and Resources

Performance is the most frequently debated yet the hardest area to generalize. Exact numbers vary enormously with workload, connection patterns, and configuration, so it is more important to understand tendencies and characteristics than absolute values.

Characteristicingress-nginxTraefik
Data plane languageC (NGINX)Go
Static high-load handlingVery strong (proven NGINX)Strong
Memory per connectionGenerally lowMay vary due to GC
Cost of config changeTriggers reloadDisruption-free (no reload)
Bulk route changesHeavy if reloads are frequentAlmost free

NGINX, the data plane of ingress-nginx, shows performance proven over years in static, stable high-load environments. Traefik, due to Go's garbage collection, may exhibit slight latency variation under extreme load, but for most real-world workloads the difference is hard to feel.

The more important performance difference in practice is "the cost of a configuration change." In environments where the Ingress changes often, ingress-nginx triggers reloads frequently. In environments with hundreds of microservices and frequent deployments, or multi-tenant platforms where routes are created and deleted dynamically, the reload itself can become a burden. The next section examines this difference in detail.

Dynamic Configuration and Reload Differences

This difference is the most essential distinction between the two controllers, so let us look at it separately and in depth.

The ingress-nginx Reload Model

When ingress-nginx detects a change in Ingress/Service/Endpoint, it operates in the following flow.

[K8s API] --watch--> [ingress-nginx controller]
                            |
                            v
                  generate new nginx.conf (template)
                            |
                            v
                  NGINX reload (nginx -s reload)
                            |
                            v
                  new worker processes apply new config

NGINX's reload is graceful. Existing workers finish in-flight connections and exit, and new workers receive traffic with the new config. But if reloads are very frequent, worker processes are continually created and destroyed, memory usage fluctuates, and long-lived connections (WebSocket, gRPC streams) can be affected at reload time. To mitigate this, ingress-nginx introduced an optimization that applies endpoint-only changes via Lua without a reload, but when the Ingress structure itself changes a reload is still required.

Traefik's Disruption-free Application

Traefik manages dynamic configuration as an in-memory routing table. When a CRD or Ingress changes, it atomically swaps in the new routing rules — there is no process reload and no worker recreation.

[K8s API] --watch--> [Traefik provider]
                            |
                            v
                  update dynamic config (in memory)
                            |
                            v
                  atomic swap of routing table
                            |
                  (no connection impact, no reload)

In environments where routes change dozens of times per second — for instance a serverless platform where routes are created and deleted per function, or a large multi-tenant SaaS — this disruption-free characteristic becomes a decisive advantage.

Observability

Half of operations is "seeing what is happening right now."

Traefik's Built-in Observability

Traefik builds Prometheus metrics, distributed tracing (OpenTelemetry), access logs, and a web dashboard into the controller itself. With one or two lines of configuration you can turn on a metrics endpoint and the dashboard.

# Traefik static configuration (part)
metrics:
  prometheus:
    addEntryPointsLabels: true
    addServicesLabels: true
tracing:
  otlp:
    grpc:
      endpoint: otel-collector:4317
api:
  dashboard: true

In the dashboard you can see currently registered routers, services, middlewares, and health status at a glance, so you can quickly confirm "how my routing rules were actually applied." However, the dashboard must always be protected with authentication in production.

ingress-nginx Observability

ingress-nginx also provides Prometheus metrics and can leverage NGINX's rich log variables. Tracing and a dashboard, however, are not built in by default and largely require separate setup (for example, enabling a tracing module or importing a Grafana dashboard). On the other hand, being able to reuse the mature logging/monitoring assets of the NGINX ecosystem is a strength. Per-VirtualServer/Upstream metrics, response-time histograms, and the like are things SREs can handle comfortably.

Ecosystem and Governance

Relationship to Gateway API

This is the most important context in 2026. The Kubernetes Ingress API is frozen — no new features are being added — and Gateway API has established itself as the next-generation standard. Gateway API offers role-based separation (Infrastructure Provider / Cluster Operator / Application Developer), expressive routing, and a unified L4/L7 model.

  • Traefik: supports Gateway API as a provider, and you can use it alongside its own CRD (IngressRoute). It is actively embracing the cloud-native standard.
  • ingress-nginx: as the project moved into maintenance mode, a migration trend is forming toward Gateway-API-based successor projects in the Kubernetes ecosystem (for example, Gateway API implementations such as NGINX Gateway Fabric). For a new adoption, this is the time to seriously consider a Gateway-API-based implementation.

In short, over the long term both camps are converging on Gateway API, and for a new project you should consider "Ingress or Gateway API" together with the controller choice.

Maintenance State and Security

ingress-nginx has been extremely widely used, but it has a history of insufficient project maintenance resources, a move into maintenance mode, and serious security vulnerabilities related to the config-injection path (for example, a series of CVEs disclosed in 2025). This does not mean ingress-nginx is bad — it means you must evaluate the security operations burden and long-term roadmap before adopting it anew. Traefik is a project with active commercial backing (Traefik Labs), with steady feature additions and documentation.

Learning Curve

Perspectiveingress-nginxTraefik
Initial entryEasy (just know the Ingress standard)Moderate (must learn CRD concepts)
Simple routingVery fastFast
Complex policiesSteepens as annotations/snippets pile upExpands gently with Middleware
TroubleshootingNeeds nginx.conf debugging knowledgeIntuitive via the dashboard
Standard affinityThe Ingress standard as-isChoice of Ingress or CRD

ingress-nginx has a low barrier to entry because you can start immediately if you know standard Ingress. But as requirements grow complex, annotations and snippets accumulate and difficulty rises steeply. Traefik has some learning cost upfront because you need to learn the CRD and static/dynamic configuration concepts, but once you do, even complex policies expand gently.

Implementing the Same Requirement on Both Sides

Enough theory. Let us look side by side at how the same requirement is implemented on each side. The requirements are as follows.

  1. Route the /api path of app.example.com to api-svc, and everything else to web-svc.
  2. Force a redirect from HTTP to HTTPS.
  3. Apply Basic Auth to the /api path.
  4. Limit requests per second.

ingress-nginx Implementation

apiVersion: v1
kind: Secret
metadata:
  name: api-basic-auth
type: Opaque
data:
  # user/password generated with htpasswd (base64)
  auth: dXNlcjokYXByMSRleGFtcGxlaGFzaA==
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example-ingress
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: 'true'
    nginx.ingress.kubernetes.io/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: 80
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-svc
                port:
                  number: 80

Since Basic Auth must apply only to the /api path, the common pattern in ingress-nginx is to split the path requiring auth into a separate Ingress object and attach the auth annotations only to that object.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example-api-auth
  annotations:
    nginx.ingress.kubernetes.io/auth-type: basic
    nginx.ingress.kubernetes.io/auth-secret: api-basic-auth
    nginx.ingress.kubernetes.io/auth-realm: 'Authentication Required'
    nginx.ingress.kubernetes.io/ssl-redirect: 'true'
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-svc
                port:
                  number: 80

You can see that when policies differ per path, you must split the Ingress object or the annotations grow complex.

Traefik Implementation

apiVersion: v1
kind: Secret
metadata:
  name: api-basic-auth
type: Opaque
data:
  users: dXNlcjokYXByMSRleGFtcGxlaGFzaA==
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: api-auth
spec:
  basicAuth:
    secret: api-basic-auth
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: ratelimit
spec:
  rateLimit:
    average: 20
    burst: 40
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: https-redirect
spec:
  redirectScheme:
    scheme: https
    permanent: true

Now we compose middlewares per path in the routing. The HTTPS redirect applies at the HTTP EntryPoint (web), while auth and rate limiting apply per path.

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: example-route
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`app.example.com`) && PathPrefix(`/api`)
      kind: Rule
      middlewares:
        - name: api-auth
        - name: ratelimit
      services:
        - name: api-svc
          port: 80
    - match: Host(`app.example.com`)
      kind: Rule
      middlewares:
        - name: ratelimit
      services:
        - name: web-svc
          port: 80
  tls:
    certResolver: letsencrypt

The difference is clear. In Traefik, policies (auth, rate limiting, redirect) are separated into reusable objects, and a route composes the policies it needs. Even when applying different policies per path, there is no need to split the Ingress object. ingress-nginx, by contrast, is more concise in simple cases, but as per-path policy branching grows, splitting objects and accumulating annotations becomes unavoidable.

Operations and Tuning

ingress-nginx Operational Points

  • worker-processes / worker-connections: tune NGINX worker settings via ConfigMap. Tune the worker count to fit node resources.
  • proxy buffers/timeouts: adjust proxy-body-size, proxy-read-timeout, and so on to handle large uploads or slow backends.
  • reload frequency management: if deployments are frequent, verify that endpoint changes are handled via the Lua path, and reduce unnecessary Ingress changes.
  • keepalive: tune upstream keepalive to increase connection reuse.
apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
data:
  worker-processes: 'auto'
  max-worker-connections: '16384'
  proxy-body-size: '100m'
  upstream-keepalive-connections: '320'
  use-gzip: 'true'

Traefik Operational Points

  • EntryPoint tuning: set timeouts (readTimeout, writeTimeout, idleTimeout) at the EntryPoint level.
  • Replicas and ACME: scaling out while using built-in ACME requires a strategy for sharing certificate storage. For HA, consider cert-manager or external issuance.
  • Provider throttle: when configuration changes flood in, batch updates with providersThrottleDuration.
  • Dashboard security: in production, always protect the dashboard with authentication/network policy.
# Traefik static configuration (part)
entryPoints:
  websecure:
    address: ':443'
    transport:
      respondingTimeouts:
        readTimeout: 60s
        writeTimeout: 60s
        idleTimeout: 180s
providers:
  kubernetesCRD:
    allowCrossNamespace: false
  providersThrottleDuration: 2s

Pitfalls and Troubleshooting

Here are pitfalls I have actually run into in operations.

Common Pitfalls in ingress-nginx

  • Snippet injection disabled: for security hardening, allow-snippet-annotations is trending toward being disabled by default. Configurations that depended on snippets can suddenly be ignored, leading to incidents. Always check on upgrade.
  • rewrite-target and regex: the combination of rewrite-target and path capture groups frequently causes mistakes. You must understand pathType and regex behavior precisely.
  • reload storms: in large environments, frequent deployments trigger reloads, memory fluctuates, and long-lived connections can drop.
  • CVE response: you must follow security patch releases quickly. Under maintenance mode, reflect the patch cadence in your operations policy.

Common Pitfalls in Traefik

  • CRD version mismatch: if the traefik.io API version and the chart version diverge, IngressRoute is ignored. Always match the installed CRD version to the chart.
  • ACME storage contention: sharing acme.json across multiple replicas causes issuance contention. For HA, a separate certificate strategy is safer.
  • Cross-namespace references: referencing a middleware from another namespace requires explicit allow settings. The default is to block.
  • Dashboard exposure: exposing the dashboard externally without authentication is a common incident. Always protect it.

Common Debugging Flow

# Check controller logs
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller
kubectl logs -n traefik deploy/traefik

# Check actually applied config (ingress-nginx)
kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- cat /etc/nginx/nginx.conf

# Check route status (Traefik dashboard or API)
kubectl port-forward -n traefik deploy/traefik 8080:8080

# Check ingress resources and events
kubectl describe ingress example-ingress
kubectl get events --sort-by=.lastTimestamp

Migration Considerations

Things to consider when moving from one to the other.

  • Annotation vs CRD mapping: there are cases where ingress-nginx annotations are hard to map one-to-one onto Traefik Middleware. You must redesign policies such as rewrite, auth, and rate-limit as middlewares.
  • Running with separate IngressClass: you can run both controllers at once and gradually shift traffic via IngressClass for a disruption-free migration.
  • Reusing TLS infrastructure: if you were using cert-manager, configure Traefik to reference the Secrets that cert-manager created, and you can move without reissuing certificates.
  • Considering Gateway API at the same time: if you are migrating, it is advantageous in the long run to also evaluate going straight to Gateway API instead of Ingress.

Decision Table

Situation/RequirementRecommendation
Simple routing with standard Ingress onlyingress-nginx or a Gateway API implementation
Many complex, reusable per-path policiesTraefik (Middleware)
Frequent disruption-free dynamic routing (serverless/multi-tenant)Traefik
Want built-in automatic TLS and a dashboardTraefik
HA wildcard/DNS-01/internal-PKI certificatesingress-nginx + cert-manager
Team with deep NGINX operations experienceingress-nginx
New greenfield project (long-term standard focus)Consider Gateway API first
Minimizing security operations burden is top priorityAn actively maintained option (Traefik/Gateway implementation)

Closing

Let us return to the beginning. The question "which is better, Traefik or ingress-nginx" has no single answer. The two controllers solve the same problem with different philosophies.

Summarized by scenario:

  • A team familiar with NGINX whose primary need is simple routing still finds ingress-nginx a fast, familiar choice. But the maintenance-mode status and the security-patch burden must be reflected in your operations policy.
  • If you need refined per-path policies, disruption-free dynamic routing, and built-in TLS/observability, Traefik fits more naturally. Pay the CRD learning cost once, and you can handle complexity gently.
  • If HA certificate management, wildcards, and internal PKI are central, a configuration with cert-manager as the certificate layer is stable regardless of which controller you use.
  • For a greenfield project starting fresh in 2026, face the fact that the Ingress API is frozen, and I recommend considering a Gateway-API-based implementation as your first choice. Traefik supports Gateway API, and the ingress-nginx camp is also moving toward Gateway API implementations.

In the end, the key is to choose at the intersection of "your team's operating model, the complexity of your requirements, and your long-term standards strategy." A controller is only a tool, and a good choice comes not from the superiority of the tool but from its fit with the context.

References