Kong Gateway Architecture: How an API Gateway Lives Inside Nginx
Prerequisites
- ›Basic understanding of reverse proxies and API gateways
- ›Familiarity with Nginx concepts (worker processes, configuration directives, upstream/downstream)
- ›Basic Lua syntax (tables, metatables, require/module system)
Kong Gateway Architecture: How an API Gateway Lives Inside Nginx
Most API gateways are standalone applications that happen to proxy HTTP traffic. Kong is different. It doesn't just sit in front of Nginx — it lives inside Nginx. Every line of Kong's request-handling logic runs as Lua code within Nginx worker processes, executed at precise points in Nginx's request lifecycle. This architecture gives Kong the raw performance of Nginx's event loop while layering on the flexibility of a fully programmable plugin system.
This article — the first in a seven-part series — explains how Kong embeds itself into Nginx, maps its Lua handlers to Nginx phases, organizes its codebase, and bootstraps the runtime environment that all subsequent request processing depends on.
OpenResty: Nginx as a Lua Application Server
Kong doesn't use Nginx directly. It uses OpenResty, a distribution of Nginx bundled with the lua-nginx-module that allows Lua code to execute inside Nginx worker processes. OpenResty exposes Nginx's internal phases as *_by_lua_block directives — hooks where Lua code can run at specific points in request processing.
This is not an embedded scripting afterthought. The lua-nginx-module provides a coroutine-based cosocket API that lets Lua code perform non-blocking I/O (database queries, HTTP calls, DNS lookups) without leaving the Nginx event loop. Every Kong plugin that calls an upstream API or queries a database does so through this cosocket layer, which means thousands of concurrent connections can be served by a single worker process.
The key insight is that Kong's Lua code runs inside the Nginx process, not alongside it. There's no inter-process communication overhead, no serialization boundary. The ngx global is available everywhere, and shared memory zones (lua_shared_dict) provide fast inter-worker data sharing.
flowchart TD
subgraph "Nginx Master Process"
A[Master: manages workers]
end
subgraph "Nginx Worker Process 1"
B[Event Loop]
C[lua-nginx-module]
D[Kong Lua Code]
B --> C --> D
end
subgraph "Nginx Worker Process N"
E[Event Loop]
F[lua-nginx-module]
G[Kong Lua Code]
E --> F --> G
end
A --> B
A --> E
H[lua_shared_dict: kong_db_cache] <-.-> D
H <-.-> G
The Phase-Driven Lifecycle: Kong's Hook into Nginx
The Nginx template at kong/templates/nginx_kong.lua is where Kong wires itself into Nginx. Each *_by_lua_block directive delegates to a method on the global Kong table:
init_by_lua_block {
Kong = require 'kong'
Kong.init()
}
init_worker_by_lua_block {
Kong.init_worker()
}
Further down in the server block, request-handling phases are wired similarly at lines 145–163:
rewrite_by_lua_block { Kong.rewrite() }
access_by_lua_block { Kong.access() }
header_filter_by_lua_block { Kong.header_filter() }
body_filter_by_lua_block { Kong.body_filter() }
log_by_lua_block { Kong.log() }
The balancer is wired via balancer_by_lua_block inside the upstream block at line 85. Here's the complete phase mapping:
| Nginx Directive | Kong Handler | Responsibility |
|---|---|---|
init_by_lua_block |
Kong.init() |
Schema loading, DB connect, router build |
init_worker_by_lua_block |
Kong.init_worker() |
Timer setup, cache warmup, clustering |
ssl_certificate_by_lua_block |
Kong.ssl_certificate() |
Dynamic TLS certificate selection |
rewrite_by_lua_block |
Kong.rewrite() |
Workspace detection, global plugin execution |
access_by_lua_block |
Kong.access() |
Routing, authentication, plugin execution |
balancer_by_lua_block |
Kong.balancer() |
Upstream target selection, retries |
header_filter_by_lua_block |
Kong.header_filter() |
Response header manipulation |
body_filter_by_lua_block |
Kong.body_filter() |
Response body transformation |
log_by_lua_block |
Kong.log() |
Logging, metrics, cleanup |
Every one of these handlers is defined in kong/init.lua, the central file of the entire codebase.
flowchart LR
A[Client Request] --> B[rewrite]
B --> C[access]
C --> D[balancer]
D --> E[Upstream]
E --> F[header_filter]
F --> G[body_filter]
G --> H[log]
H --> I[Client Response]
Directory Structure and Module Map
The kong/ directory is organized by responsibility. Understanding this map is essential for navigating the codebase:
| Directory | Purpose |
|---|---|
kong/cmd/ |
CLI commands (start, stop, reload, migrations) |
kong/conf_loader/ |
Configuration loading, validation, and merging |
kong/db/ |
Database layer: schemas, DAOs, strategies (Postgres, LMDB) |
kong/runloop/ |
Core request processing: handler, router, balancer, plugins iterator |
kong/router/ |
Route matching engine (traditional and expressions-based) |
kong/pdk/ |
Plugin Development Kit — stable API surface for plugins |
kong/plugins/ |
Bundled plugin implementations (45 plugins) |
kong/clustering/ |
Hybrid mode: CP/DP communication, config sync |
kong/llm/ |
AI gateway: LLM provider drivers and adapters |
kong/api/ |
Admin API endpoint generation and routing |
kong/tools/ |
Utility modules (gzip, string, UUID, time, etc.) |
kong/templates/ |
Nginx config templates and default configuration |
The bundled plugins are enumerated in kong/constants.lua — a flat list of 45 plugin names ranging from jwt and key-auth to the newer ai-proxy and ai-prompt-guard plugins. This list drives plugin discovery during initialization:
local plugins = {
"jwt", "acl", "correlation-id", "cors", "oauth2",
-- ... 36 more plugins ...
"ai-proxy", "ai-prompt-decorator", "standard-webhooks", "redirect"
}
Tip: The
constants.luafile is worth bookmarking. Beyond the plugin list, it definesCORE_ENTITIES(the schema loading order matters — dependencies first),ENTITY_CACHE_STOREmappings, protocol definitions, and clustering constants.
The Global Kong Object and Runtime Patches
Before any request processing can happen, Kong needs two things: a global kong object that serves as the shared namespace, and a set of runtime patches that adjust Lua/ngx behavior.
The global kong object is created in kong/global.lua via _GLOBAL.new():
function _GLOBAL.new()
return {
version = KONG_VERSION,
version_num = KONG_VERSION_NUM,
configuration = nil,
}
end
This bare table gets progressively populated during initialization. By the time requests are being served, kong.db, kong.cache, kong.core_cache, kong.worker_events, kong.cluster_events, kong.dns, kong.router, and more are all attached to this object. The PDK namespaces (kong.request, kong.response, kong.service, etc.) are initialized via _GLOBAL.init_pdk() at line 161.
sequenceDiagram
participant init.lua
participant global.lua
participant PDK
init.lua->>global.lua: _GLOBAL.new()
global.lua-->>init.lua: bare kong table {version, version_num}
init.lua->>global.lua: _GLOBAL.init_pdk(kong, config)
global.lua->>PDK: PDK.new(config, kong)
PDK-->>global.lua: attaches kong.request, kong.response, etc.
init.lua->>init.lua: kong.db = DB.new(config)
init.lua->>init.lua: kong.dns = dns_client
The runtime patches in kong/globalpatches.lua are applied once during the very first require. Three patches are particularly interesting:
-
Blocking sleep in init phases (lines 51–83):
ngx.sleepis non-blocking by design, but duringinit_worker, the coroutine-based sleep isn't available. Kong replaces it with LuaSocket's blockingsocket.sleep()— but only whenngx.get_phase()returnsinitorinit_worker. -
cJSON precision:
cjson_safe.encode_number_precision(16)ensures floating-point numbers survive JSON round-trips without precision loss. -
Protobuf defaults:
pb.option("decode_default_array")ensures empty arrays in protobuf messages decode as[]rather thannilin JSON encoding.
Tip: The globalpatches file is also where
_G._KONGis set — a minimal table with just_NAMEand_VERSION. Don't confuse_G._KONG(a global metadata marker) with thekongPDK object.
Request Lifecycle Overview
With the architecture in place, let's trace a single HTTP request at the highest level. A client sends GET /api/users to Kong's proxy port:
-
Rewrite phase (
Kong.rewrite()): Sets up the request context (ngx.ctx), records timing, detects the workspace, runs global-scope plugins. -
Access phase (
Kong.access()): The runloop'saccess.before()executes the router to match the request to a Route and Service. Plugins run in priority order — authentication plugins verify credentials, rate-limiters check quotas, transformers modify the request. The balancer is prepared with upstream connection details. -
Balancer phase (
Kong.balancer()): Selects the specific upstream target (IP:port), handles DNS resolution, sets timeouts, and manages connection pooling. On retries, this phase re-executes. -
Upstream: Nginx forwards the request to the selected upstream target.
-
Header filter phase (
Kong.header_filter()): Processes the upstream response headers. Plugins can add, remove, or modify headers. -
Body filter phase (
Kong.body_filter()): Processes the response body in streaming chunks. Called potentially multiple times for chunked responses. -
Log phase (
Kong.log()): All timing data is finalized. Logging plugins (http-log, file-log, datadog) serialize and dispatch the request record. The context is released.
sequenceDiagram
participant Client
participant Rewrite as Kong.rewrite()
participant Access as Kong.access()
participant Balancer as Kong.balancer()
participant Upstream
participant HdrFilter as Kong.header_filter()
participant BodyFilter as Kong.body_filter()
participant Log as Kong.log()
Client->>Rewrite: GET /api/users
Rewrite->>Access: workspace set
Access->>Access: Router match → Route + Service
Access->>Access: Plugins execute (auth, rate-limit, ...)
Access->>Balancer: balancer_data prepared
Balancer->>Upstream: IP:port selected
Upstream-->>HdrFilter: 200 OK + headers
HdrFilter-->>BodyFilter: modified headers
BodyFilter-->>Log: body chunks streamed
Log-->>Client: response delivered
Each phase handler follows a pattern visible in kong/init.lua: set up timing markers, call runloop.<phase>.before(), iterate over plugins, call runloop.<phase>.after(), record timing. This sandwich pattern is the backbone of Kong's extensibility — the runloop provides infrastructure, and plugins provide behavior.
What's Next
This article established the foundation: Kong is Lua code running inside Nginx worker processes, hooked into every phase of request processing. In Part 2, we'll trace the complete boot sequence — from the moment you type kong start through CLI dispatch, configuration loading, Nginx template rendering, and the init/init_worker phases that prepare Kong to serve traffic.