How do Kong, Envoy Gateway and Traefik work under the hood?
Teams put an API gateway in front of microservices so that TLS termination, routing, authentication and rate limiting live in one enforced layer instead of being reimplemented, inconsistently, in every service. All three gateways do this. They differ in how configuration reaches the proxy and what fails when something breaks.
Kong
Kong runs on NGINX and OpenResty, with request handling written in Lua. Features are plugins attached to services, routes or consumers. On Kubernetes, the Kong Ingress Controller watches cluster resources, translates them into Kong configuration, and pushes that configuration to the proxies. Kong applies changes in memory by rebuilding its router rather than reloading NGINX, so in-flight connections are not dropped. If the controller dies, proxies keep serving their last configuration. Routes simply stop updating.
Envoy Gateway
Envoy Gateway is a Kubernetes controller. It reads Gateway API resources and programs a fleet of Envoy proxies over xDS, Envoy's streaming configuration protocol. Envoy applies updates without restarting. When a listener changes, it drains the old one gracefully. If the controller restarts, Envoy keeps its last-known configuration and resynchronizes when the connection returns.
Traefik
Traefik is a single Go binary. Its providers watch Kubernetes, Docker, files and other sources, and it swaps the routing configuration in memory. The control plane and data plane share a process, so the footprint is small. The trade-off is that a crash affects both discovery and traffic, which means running multiple replicas matters.
Raw proxying ability is broadly similar across the three. The rest of this article focuses on what differs: configuration model, extension mechanism and operational footprint.
KONG ENVOY GATEWAY TRAEFIK
K8s API: Ingress, CRDs K8s API: Gateway API K8s | Docker | file ...
| | |
v v v
+----------------------+ +----------------------+ +----------------------+
| Kong Ingress | | Envoy Gateway | | Traefik (Go binary) |
| Controller (CP) | | controller (CP) | | providers (CP) |
+----------------------+ +----------------------+ | | |
| push config | xDS stream | v |
v v | routing config |
+----------------------+ +----------------------+ | swapped in memory |
| Kong proxy (DP) | | Envoy proxy fleet | | (DP) |
| NGINX + OpenResty | | (DP) | | |
| Lua plugins | | updates w/o restart, | | CP + DP share one |
| router rebuilt in | | old listener drains | | process, small |
| memory, no reload | | gracefully | | footprint |
+----------------------+ +----------------------+ +----------------------+
^ ^ ^
| clients | clients | clients
CP down: proxies serve CP down: Envoy keeps Crash: discovery and
last config; routes last-known config; traffic both stop; run
stop updating resyncs on reconnect multiple replicas
Control plane (CP) and data plane (DP) layout for Kong, Envoy Gateway and Traefik. The diagram shows how configuration reaches each proxy and what happens to traffic when the controller fails.
How does the configuration model differ between Kong and Envoy Gateway (and Traefik)?
Envoy Gateway treats the Kubernetes Gateway API as its native interface. A platform team defines a GatewayClass and a Gateway (listeners, ports, TLS), and application teams attach HTTPRoute objects to it. This role split is the main reason the API exists: cluster operators own the entry point, and service owners own their routes without editing shared config. Kong and Traefik also implement Gateway API, but both predate it and still ship their own models. Kong has its Ingress controller with KongPlugin and KongConsumer CRDs plus annotations. Traefik has IngressRoute and Middleware CRDs.
The core routing spec ports cleanly. An HTTPRoute that matches a path prefix, sets a header and splits traffic by weight across two backends should behave the same on all three.
Portability ends at policy. The core spec has no authentication or rate limiting, so each project adds its own resources:
- Envoy Gateway:
SecurityPolicyhandles JWT, OIDC and basic auth.BackendTrafficPolicyhandles rate limits. Both attach to a Gateway or route through a target reference. - Kong: You define a
KongPluginand reference it from the route with thekonghq.com/pluginsannotation. - Traefik: You define a
Middlewareand reference it through anExtensionReffilter on the route.
In practice, your routes are portable but your security posture is not. A migration means rewriting every policy object, and that is usually where most of the risk lives.
Gateway API conformance and policy attachment are still changing in all three projects. Check each project's current conformance reports and documentation rather than relying on the specifics here.
# Portable core: path match, header set and weighted split are standard Gateway API.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: orders
namespace: shop
annotations:
# Kong-only: other implementations ignore this, so the rate limit silently disappears.
konghq.com/plugins: orders-rate-limit
spec:
parentRefs:
- name: public-gateway # Gateway owned by the platform team
rules:
- matches:
- path:
type: PathPrefix
value: /orders
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
set:
- name: X-Route-Owner
value: orders-team
backendRefs:
- name: orders-v1
port: 8080
weight: 90
- name: orders-v2 # canary gets 10% of traffic
port: 8080
weight: 10
---
# Kong-specific policy. On Envoy Gateway this becomes a BackendTrafficPolicy, on Traefik a Middleware.
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: orders-rate-limit
namespace: shop
plugin: rate-limiting
config:
minute: 100
policy: local
A standard HTTPRoute that should behave the same on all three gateways, with a Kong annotation and KongPlugin showing where rate limiting depends on a single implementation and would have to be rewritten during a migration.
How do you extend each gateway with custom logic, and what does it cost?
Each project offers two ways to add custom logic. You can run code inside the proxy, which is fast but couples your code to the gateway's runtime and upgrade cycle. Or you can call an external service, which adds a network hop but keeps your code independent of the proxy.
| Gateway | In-proxy options | External call options | Main ongoing cost |
|---|---|---|---|
| Kong | Lua plugins via the Plugin Development Kit; Go, Python or JavaScript through a separate plugin server process | Custom plugins that call out over HTTP | Lua and OpenResty expertise, plus PDK changes across major versions |
| Envoy Gateway | Wasm (proxy-wasm), Lua filters. Native C++ filters require a custom Envoy build. | External authorization (ext_authz), external processing (ext_proc) | Wasm toolchain maturity, plus how stable the extension policy CRDs are |
| Traefik | Go plugins interpreted at runtime, Wasm plugins, built-in middleware chaining | ForwardAuth middleware | Interpreter limits on Go packages and runtime speed |
In-proxy code runs inside the proxy's request path. A slow regex or memory leak in it therefore slows or crashes the proxy for every route. You need load tests on every gateway upgrade, and someone must own the code after its author leaves.
An external auth or processing service is usually cheaper when the logic already exists in a backend team's language. It also helps when the logic needs databases or caches, or must be deployed on a different schedule from the gateway. Your team tests and releases it like any other service.
Choose in-proxy code when:
- The logic is small and stateless, such as header rewrites or cheap token checks.
- An extra network hop per request is unacceptable.
Several advanced plugins and middlewares are available only in the commercial editions of Kong and Traefik. Examples include OIDC, advanced rate limiting and some transformations. Check what the open-source edition includes before you plan to rely on a plugin.
Authentication, rate limiting, transformation and observability compared
Authentication is the easiest concern to compare. Kong attaches auth plugins such as jwt and key-auth to routes or services. Envoy Gateway declares JWT, OIDC and API-key checks in a SecurityPolicy. Traefik provides BasicAuth and ForwardAuth middlewares, and teams often use ForwardAuth to send token checks to an external auth service.
Rate limiting differs most in where the counter state lives. A local limit keeps counters in each replica's memory. It adds no dependency, but the effective limit grows with replica count: a 100 req/s limit across four replicas allows roughly 400 req/s (illustrative estimate). A distributed limit is accurate across replicas, but each request now depends on a shared store. Envoy Gateway's global limits call a separate rate-limit service backed by Redis. Kong's rate-limiting plugin can use Redis as its counter store. You then have to decide whether the gateway fails open or closed when that store is slow or down, because otherwise a Redis incident turns into a gateway incident. Traefik's open-source RateLimit middleware counts per instance. The example below shows a local policy.
Basic transformations, such as header edits and path rewrites, work through Gateway API filters on all three. Kong also offers its request-transformer plugins. All three export Prometheus metrics, write structured access logs and support OpenTelemetry tracing.
| Concern | Kong | Envoy Gateway | Traefik |
|---|---|---|---|
| Auth | KongPlugin (jwt, key-auth) |
SecurityPolicy |
Middleware (ForwardAuth, BasicAuth) |
| Rate limit | Plugin; local or Redis | BackendTrafficPolicy; local or global service |
RateLimit middleware, per instance |
| Transformation | Plugins, HTTPRoute filters | HTTPRoute filters | Middlewares, HTTPRoute filters |
| Observability | Prometheus/OTel plugins | EnvoyProxy telemetry config |
Static config: metrics, tracing, logs |
# Envoy Gateway local rate limit attached to one HTTPRoute.
# Counters live in each Envoy replica's memory, so no Redis or
# rate-limit service sits in the request path.
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: orders-local-ratelimit
namespace: shop
spec:
# Apply the policy to the route that serves the orders API.
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: orders-api
rateLimit:
# Local: each replica enforces the limit independently.
# With 4 Envoy replicas, the effective ceiling is roughly
# 4 x 100 req/s (illustrative estimate).
# Switching to type: Global makes the limit accurate across
# replicas, but every request then depends on the external
# rate-limit service and its Redis backend.
type: Local
local:
rules:
- limit:
requests: 100
unit: Second
An Envoy Gateway BackendTrafficPolicy that applies a per-replica local rate limit of 100 requests per second to an HTTPRoute, with no external counter store.
What does each gateway cost to operate?
Operational cost is driven less by the proxy itself than by how many stateful or stateful-adjacent parts you have to keep healthy, and how hard they are to debug at 3 a.m.
Kong with a database adds PostgreSQL to your on-call surface. You own schema migrations on every upgrade, backups, and database availability. DB-less mode removes all of that, but the Admin API becomes effectively read-only. Workflows that create consumers or plugins through API calls at runtime must move to declarative config pushed through Git or the ingress controller.
Envoy Gateway runs a controller that translates Gateway API resources into xDS for a separate Envoy fleet. Both tiers must be upgraded in a compatible order. When a route misbehaves, you have to reason about generated listeners, filter chains and clusters in Envoy's config dump, and few teams already have that skill.
Traefik is a single process. That makes it the simplest to upgrade, but a bad config or a bad release reaches every route at once. If you run several replicas, use cert-manager, because built-in ACME storage is not designed to be shared across replicas.
| Option | Components you run | Upgrade risk | Certificates |
|---|---|---|---|
| Kong (DB) | Kong, PostgreSQL, controller | Migrations | cert-manager or Kong |
| Kong (DB-less) | Kong, controller | Config reload | cert-manager |
| Envoy Gateway | Controller, Envoy fleet | Version skew | cert-manager |
| Traefik | Traefik | Shared blast radius | Built-in ACME or cert-manager |
Illustrative estimate (assumed numbers, not a benchmark): monthly upkeep hours = components × hours per component. Assume 2 hours per component per month, and count the database as 3 components' worth because of backups and migrations. That gives:
- Kong (DB): (2 + 3) × 2 = 10 hours
- Kong (DB-less): 2 × 2 = 4 hours
- Envoy Gateway: 2 × 2 = 4 hours, plus debugging ramp-up
- Traefik: 1 × 2 = 2 hours
Plug in your own per-component hours to adapt this to your team.
Which Kubernetes API gateway should your team choose, and when should you avoid each?
Start from what your team must enforce at the edge, not from feature lists.
| Team situation | Choose | Avoid it when |
|---|---|---|
| External API products: consumers, keys, quotas, developer-facing policy | Kong | You run it database-backed and no one owns that database's backups, upgrades and failover |
| Platform team standardizing on Gateway API, needs Envoy-grade traffic control | Envoy Gateway | No one can read Envoy config dumps, access logs and stats when routing misbehaves |
| Small team, mostly HTTP routing, wants minimal moving parts | Traefik | Many teams share a cluster and nothing governs who owns which host and path |
The Traefik risk comes from auto-discovery. Any team can publish a route, so two teams claiming the same host or path will collide, and the gateway will not flag it as an ownership problem. If you choose Traefik in a shared cluster, add admission policy or namespace conventions first. The Envoy Gateway risk comes from its abstraction: it is thin until something breaks, and then you are debugging Envoy. Kong's database mode adds a stateful dependency to your traffic path, so give it an owner or run DB-less.
You may not need a full gateway. If you only route by host and path, plain ingress is enough. If your policy is east-west (mTLS, service-to-service retries), it belongs in a mesh, not the edge.
Assign each policy to exactly one layer. For example, the gateway authenticates external callers and the mesh handles workload identity. Retries are a common failure: if both the gateway and the mesh retry, the retry counts multiply. Rate limits configured in both layers drift apart, and on-call cannot tell which layer rejected a request.
How do you run a fair evaluation of Kong vs Envoy Gateway vs Traefik?
Treat the evaluation as a staging exercise built from your own production config, not a feature matrix. Port a representative slice of real routes, including the awkward ones: header-based matching, path rewrites, gRPC, WebSockets. Port your actual auth and rate-limit policies too. Translation friction during this step is a finding in its own right.
Before testing anything, confirm each controller accepted the config. Run kubectl get gateway -A and kubectl describe httproute <name> -n <ns>, then check that the status conditions show Accepted, ResolvedRefs and Programmed as True. A route that is silently rejected produces misleading results.
Load-test with your own traffic shape: realistic request sizes, connection reuse, TLS handshakes and the share of authenticated calls. Published benchmarks usually measure a bare proxy, while your latency will come mostly from plugin chains, TLS settings and policy evaluation. A generic run such as k6 run script.js or `hey -z 5m -c 50 is enough if the script replays production-like paths.
Then break things while traffic flows:
- Restart the controller pod.
- Apply a route change under load.
- Kill the rate-limit backend (Redis or an external service).
- Rotate a certificate.
Pass/fail checklist
- Pass: p99 latency stays within your budget with the full policy chain enabled.
- Pass: controller restarts and config reloads cause no dropped or reset connections.
- Pass: a rate-limit backend outage follows a documented fail-open or fail-closed behavior that you chose deliberately.
- Pass: rotated certificates are served without a restart.
- Fail: any policy you could not express without custom code you are unwilling to own.
#!/usr/bin/env bash
# Staging check: confirm config was accepted, then restart the controller under load.
set -euo pipefail
NS=staging # namespace holding the Gateway and routes
GW=eval-gateway # Gateway under test
ROUTE=api-route # a representative HTTPRoute
CTRL_NS=gateway-system # where the controller runs (varies per product)
CTRL_DEPLOY=gateway-controller # controller Deployment name (varies per product)
URL=
# 1. Gateway must report Programmed=True before any numbers mean anything.
kubectl wait --for=condition=Programmed gateway/"$GW" -n "$NS" --timeout=120s
# 2. HTTPRoute conditions live under status.parents; expect Accepted and ResolvedRefs True.
CONDS=$(kubectl get httproute "$ROUTE" -n "$NS" \
-o jsonpath='{range .status.parents[*].conditions[*]}{.type}={.status}{"\n"}{end}')
echo "$CONDS"
if [ -z "$CONDS" ] || echo "$CONDS" | grep -q '=False'; then
echo "Route not fully accepted; fix config before load testing" >&2
exit 1
fi
# 3. Run steady load in the background, then restart the controller mid-run.
hey -z 5m -c 50 "$URL" > hey-controller-restart.txt &
LOAD_PID=$!
sleep 60
kubectl rollout restart deployment/"$CTRL_DEPLOY" -n "$CTRL_NS"
kubectl rollout status deployment/"$CTRL_DEPLOY" -n "$CTRL_NS" --timeout=180s
wait "$LOAD_PID"
# 4. Pass only if every response is the expected status and hey reports no errors.
grep -A10 'Status code distribution' hey-controller-restart.txt
grep -A10 'Error distribution' hey-controller-restart.txt || echo "No connection errors"
A bash script that verifies Gateway and HTTPRoute status conditions before testing, then restarts the gateway controller during a five-minute hey load run and checks the output for non-2xx responses and connection errors.
How OneEnv can help
Disclosure: OneEnv is the product behind this blog.
OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.
Applied to kong vs envoy gateway, this is where it helps:
Saves time
- Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
- One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
- Reviewers see the exact diff and the list of affected services up front, so approval is a decision rather than an investigation.
Saves cost
- Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
- Less duplicated configuration to maintain: one shared definition inherited by many services.
- Per-service approve/reject means a bad shared value can be refused where it would break something, before it is live.
- OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.
These are qualitative benefits, not measured results — how much you save depends on how many services share the value.
Want to try it? Start at oneenv.io.