API Gateway Selection Criteria
Picking the wrong gateway forces a painful migration once traffic scales or compliance requirements shift. This page provides a decision framework grounded in API Gateway Fundamentals & Architecture — covering control-plane design, plugin extensibility, routing capabilities, protocol translation support, and the operational trade-offs between Kong 3.x, Envoy 1.32+, Tyk 5.x, and NGINX. By the end you will have a repeatable evaluation method and concrete config examples for each gateway, not a vendor marketing comparison.
Architectural Baseline
Before evaluating any product, establish a clear model of the two planes every gateway contains:
- Control plane — the process (or cluster) that reads declarative configuration and distributes it to data-plane workers. Its job is to reconcile desired state with running state.
- Data plane — the hot path that proxies requests. It must remain available even when the control plane is unreachable, which means it must hold a local copy of the last-known good configuration.
The gap between these planes creates the most common selection mistake: teams benchmark raw request throughput without testing what happens during a configuration change. If the control plane broadcasts a full-state replacement to all workers simultaneously, every worker pauses to re-parse the config, causing latency spikes under load. Gateways that use atomic diff-based propagation — xDS incremental APIs, or Kong’s declarative deck sync — avoid this problem.
Two additional architectural dimensions shape every downstream decision:
Deployment topology — Where does the gateway sit? Edge-only, per-cluster ingress, or per-pod sidecar? Envoy is the only gateway that can cleanly serve all three roles without modification. Kong and Tyk are optimised for centralised ingress; NGINX can run at the edge but lacks dynamic routing without a commercial control plane.
Plugin execution model — Does the gateway run plugins in-process (Lua, Go native), in a subprocess (NGINX Perl/Python), or in a WebAssembly sandbox (Envoy Wasm, Kong 3.x Wasm)? In-process plugins have the lowest latency overhead but can crash the proxy on a bug; Wasm sandboxes add roughly 0.3–0.8 ms per plugin invocation but isolate faults. Settle this question before reaching the middleware chain evaluation layer, because it constrains which gateways are viable.
Primary Concept: Control-Plane Architecture Deep-Dive
Kong 3.x — Database-backed vs DB-less
Kong ships two control-plane modes. In database mode the control plane writes to PostgreSQL and workers poll for changes every db_update_frequency seconds (default: 5 s). In DB-less mode a single kong.yml declarative config is loaded at startup and reloaded on SIGHUP — no database required.
# Kong 3.x — DB-less declarative config (kong.yml)
_format_version: "3.0"
_transform: true
services:
- name: payments-api
url: http://payments-svc:8080
connect_timeout: 3000
write_timeout: 5000
read_timeout: 5000
routes:
- name: payments-v2
paths:
- /api/v2/payments
methods:
- POST
- GET
strip_path: false
plugins:
- name: rate-limiting
config:
minute: 1000
policy: redis
redis_host: redis-cluster.internal
redis_port: 6379
In production, DB-less mode is preferable for immutable infrastructure: the config is version-controlled, diff-reviewable, and applied with kong reload (zero connection drops). Database mode suits teams that need the Kong Admin API for dynamic changes at runtime.
Config propagation timing: DB-less reload completes in roughly 200 ms on a 10 k-route config; database mode depends on db_update_frequency plus PostgreSQL read latency. If your release pipeline needs sub-second propagation across 20 or more worker nodes, DB-less with a config management pipeline (deck + CI) wins.
Envoy 1.32+ — xDS Dynamic Control Plane
Envoy does not have a built-in control plane; it consumes configuration from an external xDS management server via gRPC. The xDS suite covers Listener Discovery Service (LDS), Route Discovery Service (RDS), Cluster Discovery Service (CDS), and Endpoint Discovery Service (EDS). Incremental xDS (delta xDS) sends only the changed resources, so a single route update does not re-transmit the entire routing table.
# Envoy 1.32 — static bootstrap with xDS management server reference
node:
id: edge-proxy-01
cluster: edge-ingress
dynamic_resources:
lds_config:
resource_api_version: V3
api_config_source:
api_type: GRPC
transport_api_version: V3
grpc_services:
- envoy_grpc:
cluster_name: xds-management-server
set_node_on_first_message_in_delta_stream: true
cds_config:
resource_api_version: V3
api_config_source:
api_type: GRPC
transport_api_version: V3
grpc_services:
- envoy_grpc:
cluster_name: xds-management-server
static_resources:
clusters:
- name: xds-management-server
type: STRICT_DNS
connect_timeout: 1s
load_assignment:
cluster_name: xds-management-server
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: control-plane.internal
port_value: 18000
http2_protocol_options: {}
The Envoy control plane (Istio Pilot, Contour, or a custom Go server using go-control-plane) can push updates in under 100 ms. If the xDS server becomes unreachable, Envoy continues serving traffic with the last-received configuration — a critical safety property for production.
Decision Flow: Choosing Your Gateway
The diagram below captures the primary branching logic. Work through it top-to-bottom; the first “Yes” branch that matches your requirements determines the starting recommendation.
Secondary Concept: Plugin Extensibility and Edge Cases
Lua plugins in Kong (in-process, ~0 ms overhead)
-- Kong 3.x custom plugin: enforce X-Api-Version header
local plugin = {
PRIORITY = 1000,
VERSION = "1.0.0",
}
function plugin:access(conf)
local version = kong.request.get_header("X-Api-Version")
if not version then
return kong.response.exit(400, {
message = "X-Api-Version header is required",
code = "MISSING_API_VERSION",
})
end
-- pass version downstream as a sanitised header
kong.service.request.set_header("X-Upstream-Api-Version", version)
end
return plugin
Lua plugins execute in the same Nginx worker process, so a nil-pointer bug can crash that worker. Kong 3.x introduced Wasm-based plugin execution as an alternative. Enable it with wasm: on in kong.conf and distribute the plugin as a compiled .wasm module — the sandbox prevents crashes from escaping the plugin boundary.
Envoy Wasm filter (isolated, ~0.5 ms overhead)
# Envoy 1.32 — Wasm HTTP filter
http_filters:
- name: envoy.filters.http.wasm
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
config:
name: "api-version-enforcer"
root_id: "api_version_root"
vm_config:
vm_id: "api_version_vm"
runtime: "envoy.wasm.runtime.v8"
code:
local:
filename: "/etc/envoy/filters/api_version.wasm"
allow_precompiled: true
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: '{"required_header": "X-Api-Version"}'
Wasm filters can be hot-reloaded without restarting Envoy by updating the xDS configuration to point to a new .wasm binary. The V8 sandbox isolates faults but adds roughly 0.3–0.8 ms overhead per filter invocation at p99 under contention.
Tyk 5.x — Go plugin middleware
Tyk supports compiled Go plugins loaded as shared objects (.so files). This gives native Go performance with full access to the Go standard library, but plugins must be compiled against the exact Tyk version’s Go toolchain.
// Tyk 5.x API definition — custom Go middleware
{
"api_id": "payments-api",
"name": "Payments API",
"version_data": {
"not_versioned": false,
"versions": {
"v2": {
"name": "v2",
"use_extended_paths": true,
"extended_paths": {
"go_plugins": [
{
"path": "/opt/tyk-gateway/middleware/api_version_enforcer.so",
"func_name": "EnforceApiVersion",
"method": "",
"pattern": ".*"
}
]
}
}
}
}
}
The major operational risk with Tyk Go plugins is binary compatibility: upgrading Tyk requires recompiling all plugins. Teams that update frequently should maintain a plugin build pipeline keyed to the Tyk version tag.
Comparative Implementation Table
| Gateway | Control-plane model | Plugin model | Protocol support | Key trade-off |
|---|---|---|---|---|
| Kong 3.x | DB-less declarative or PostgreSQL-backed Admin API | Lua (in-process) or Wasm sandbox | HTTP/1.1, HTTP/2, gRPC, WebSocket, TCP | Richest plugin hub; Lua crashes escape the sandbox unless Wasm mode is enabled |
| Envoy 1.32+ | External xDS server (delta xDS, sub-100 ms propagation) | Wasm (V8 sandbox) or native C++ filters | HTTP/1.1, HTTP/2, HTTP/3, gRPC, WebSocket, raw TCP | Steepest learning curve; requires running or buying an xDS control plane |
| Tyk 5.x | Tyk Dashboard (Redis-backed) or DB-less MDCB | Compiled Go .so plugins |
HTTP/1.1, HTTP/2, gRPC, GraphQL, TCP | Go plugins must be rebuilt per Tyk version; built-in developer portal is a differentiator |
| NGINX (OSS + NJS) | Static config files + nginx -s reload |
NJS (JavaScript) modules | HTTP/1.1, HTTP/2, WebSocket, raw TCP | No dynamic routing without NGINX Plus or a third-party config manager; highest raw throughput |
Operational Gotchas
1. Config propagation races in Kong database mode
When multiple Kong nodes share a PostgreSQL control plane and db_update_frequency is set to the default 5 s, nodes can hold divergent route tables for up to 10 s after a change is committed. Under A/B canary deployments this can route the same user to different route versions within the same session.
Remediation: Either switch to DB-less mode with declarative config and kong reload, or reduce db_update_frequency to 1 s and monitor PostgreSQL read latency — values below 50 ms are safe.
2. Envoy xDS connection loss
If the xDS management server becomes unreachable, Envoy holds the last good configuration indefinitely. This is the correct safe default. The failure mode engineers miss is startup without an xDS server — Envoy will not serve traffic until it receives at least one LDS response. Set initial_fetch_timeout and configure static fallback listeners for health-check endpoints.
# Envoy 1.32 — xDS fetch timeout + static health-check listener
dynamic_resources:
lds_config:
initial_fetch_timeout: 10s # wait 10 s for LDS before failing
resource_api_version: V3
api_config_source: {} # abbreviated — see bootstrap above
static_resources:
listeners:
- name: health-check-static
address:
socket_address: { address: 0.0.0.0, port_value: 8888 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: health
route_config:
virtual_hosts:
- name: local
domains: ["*"]
routes:
- match: { prefix: "/healthz" }
direct_response:
status: 200
body:
inline_string: "ok"
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
3. Tyk plugin binary drift
A Tyk 5.x upgrade that bumps the internal Go version will silently fail to load plugins compiled against the previous toolchain. The gateway logs plugin could not be loaded and falls back to passing requests without that middleware — a silent security regression if the plugin enforced authentication and token validation.
Remediation: Pin the Tyk version in your docker-compose.yml or Helm values, gate Tyk upgrades on a plugin build step in CI, and add a startup health check that verifies each .so is loadable before the process accepts traffic.
4. NGINX rate-limiting state not shared across workers
NGINX OSS limit_req_zone stores counters in a per-worker shared memory zone (zone=api_rate:10m). Under high concurrency each worker has a slightly different view of the counter, meaning the actual request rate allowed is approximately limit × worker_count. This makes rate limiting and throttling inaccurate for strict per-user quotas.
# NGINX — shared memory rate-limit zone (OSS: per-instance, not cluster-wide)
http {
limit_req_zone $http_x_api_key zone=per_key:10m rate=100r/s;
server {
location /api/ {
limit_req zone=per_key burst=20 nodelay;
limit_req_status 429;
proxy_pass http://upstream_svc;
}
}
}
For cluster-wide enforcement use NGINX Plus with a shared zone sync peer, or route to Kong or Tyk where Redis-backed rate limiting is first-class.
5. Missing strip_path on Kong routes causes double-path upstream calls
A common mistake when migrating from a reverse proxy is forgetting strip_path: true on Kong routes with a prefix match. The upstream service receives /api/v2/payments/api/v2/payments/123 because Kong appends the full request path to the upstream URL.
Signal: HTTP 404 from upstream with no matching route in the upstream logs.
Fix: Set strip_path: true (the Kong default) or use path_handling: v1 for explicit path rewriting.
Production Configuration Checklist
- Control-plane propagation tested under load: push a config change while sustaining 5 k req/s and verify zero 502s
- Plugin timeout budgets set per stage:
timeout_budget_ms(Kong) or per-filter timeout in the EnvoyHttpConnectionManager - Wasm sandbox enabled for any third-party plugins (Kong:
wasm: on; Envoy:runtime: envoy.wasm.runtime.v8) - DB-less config committed to version control and applied through CI (
deck syncfor Kong,tyk-syncfor Tyk) -
initial_fetch_timeoutset on all Envoy xDS configs with static health-check listeners as fallback - Tyk plugin build pipeline gates gateway upgrades on successful
.socompilation and load test - NGINX rate-limit zones replaced with Redis-backed counters if per-user enforcement is required
-
strip_pathbehaviour verified for every Kong route by replaying sample requests against staging - Zero-trust mTLS configured between gateway and upstream services, not just at the public edge
- Distributed tracing headers (
traceparent,tracestate) injected by the gateway and validated end-to-end through at least two upstream hops
FAQ
What is the most important factor when choosing an API gateway?
Control-plane architecture is typically the deciding factor. A gateway whose control plane cannot propagate configuration changes atomically will cause request drops during rollouts. Evaluate whether the gateway uses eventual-consistency-safe diff algorithms or full-state replacements, and whether the data plane can continue serving traffic independently if the control plane becomes temporarily unavailable.
When should I choose Envoy over Kong?
Choose Envoy when you need xDS-native dynamic configuration, deep gRPC and HTTP/2 support with precise flow-control, or sidecar-less service-mesh integration with Istio. Choose Kong when your team needs a rich, stable plugin ecosystem with a familiar Lua or Go SDK, a GUI admin API, and a gentler operational learning curve.
Can I run multiple API gateways in the same platform?
Yes, and it is common at scale. A typical pattern is Envoy or NGINX at the internet edge for raw TLS termination and DDoS absorption, Kong or Tyk as the API management layer for policy enforcement, and a service mesh sidecar for east-west traffic between microservices. Each layer handles its own concerns without duplicating policy.
Related
- How API Gateways Differ from Load Balancers — the layer-4 vs layer-7 distinction to settle before comparing products
- Kong vs Tyk vs Envoy for Microservices — deeper vendor comparison with benchmark data
- Security Boundaries & Zero Trust — mTLS, JWT validation, and OPA policy enforcement at the gateway edge
- High Availability Topologies — control-plane HA and data-plane redundancy patterns
- Implementing mTLS at the Gateway Edge — step-by-step certificate provisioning and rotation for each gateway