Read OSS

Kong Gateway Architecture: How an API Gateway Lives Inside Nginx

Intermediate

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.lua file is worth bookmarking. Beyond the plugin list, it defines CORE_ENTITIES (the schema loading order matters — dependencies first), ENTITY_CACHE_STORE mappings, 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:

  1. Blocking sleep in init phases (lines 51–83): ngx.sleep is non-blocking by design, but during init_worker, the coroutine-based sleep isn't available. Kong replaces it with LuaSocket's blocking socket.sleep() — but only when ngx.get_phase() returns init or init_worker.

  2. cJSON precision: cjson_safe.encode_number_precision(16) ensures floating-point numbers survive JSON round-trips without precision loss.

  3. Protobuf defaults: pb.option("decode_default_array") ensures empty arrays in protobuf messages decode as [] rather than nil in JSON encoding.

Tip: The globalpatches file is also where _G._KONG is set — a minimal table with just _NAME and _VERSION. Don't confuse _G._KONG (a global metadata marker) with the kong PDK 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:

  1. Rewrite phase (Kong.rewrite()): Sets up the request context (ngx.ctx), records timing, detects the workspace, runs global-scope plugins.

  2. Access phase (Kong.access()): The runloop's access.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.

  3. 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.

  4. Upstream: Nginx forwards the request to the selected upstream target.

  5. Header filter phase (Kong.header_filter()): Processes the upstream response headers. Plugins can add, remove, or modify headers.

  6. Body filter phase (Kong.body_filter()): Processes the response body in streaming chunks. Called potentially multiple times for chunked responses.

  7. 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.