Sign in →
1 min read

Kong Gateway Integration

Enable the Aforo metering plugin on your existing Kong cluster. Zero application code changes required.

Updated 2026-11-11Suggest edits

Task Overview#

JIRA-READY TASK
Enable the Aforo metering plugin on your existing Kong cluster. Verify handshake with SRE team before promoting to production.
Estimated time: 15 minutes. Target: Platform Engineer + SRE Verification.
kong-deployment-architecture.flowLIVE
INGRESS
HTTP In
client request
TLS termination at Kong gateway. Headers parsed and route matched.
0ms
ACCESS
Entitlement
access phase
Local Redis lookup for workspace entitlement + L1-L3 margin check.
<2ms
PROXY
Backend
upstream call
Request forwarded to your origin service. Response streamed back.
passthrough
LOG
Metering
log_by phase
Async event capture after response sent. Zero latency added to client.
0ms added
BUFFER
Batch Flush
every 5s
50 events per batch (flush_count), or every flush_interval_ms.
5s window
EGRESS
Aforo Ingest
api.aforo.ai
HTTPS POST with retry. Acknowledged by ingest pipeline.
<50ms
kong → aforo metering pipeline · zero client-perceived latency// p95 < 5ms

Prerequisites#

REQUIRED
Kong Gateway
OSS 3.x+ or Enterprise 3.x+
REQUIRED
Admin API access
HTTP access to Kong Admin API (default: localhost:8001)
REQUIRED
Aforo API Key
sk_live_* key from Workspace Admin → API Keys
OPTIONAL
Redis (optional)
For local edge cache. Falls back to in-memory LRU if unavailable.

Step 1: Get the Plugin#

The Kong plugin lives in the Aforo SDK distribution repo at aforo-gateway-plugins/kong/. It is not yet on the public LuaRocks registry, so build and install it from source. Clone the repo, then make the plugin from the rockspec in that folder:

terminal
# Clone the SDK distribution repo and enter the Kong plugin folder
git clone https://github.com/aforoai/SDKs.git
cd SDKs/aforo-gateway-plugins/kong

# Install the runtime dependency used by the log-phase flush
luarocks install lua-resty-http

# Build + install the plugin from the rockspec in this folder
# (run 'ls *.rockspec' to confirm the version on your checkout)
luarocks make kong-plugin-aforo-metering-2.0.2-1.rockspec

# Verify the plugin is loadable
kong version -vv 2>&1 | grep aforo

Then load it in kong.conf and reserve the buffer the log phase writes to — plugins = bundled,aforo-metering and nginx_http_lua_shared_dict = aforo_buffer 10m — then run kong reload. The shared-dict line is mandatory; without it the log phase drops every event.

Step 2: Configure#

Add the plugin to your kong.yml declarative config or apply via Admin API:

Option A: Declarative (kong.yml) — Production Ready

kong.yml
plugins:
  - name: aforo-metering
    config:
      # ── Core (required) ──────────────────────
      aforo_endpoint: "https://api.aforo.ai/v1/ingest/batch"
      api_key: "{vault://env/AFORO_API_KEY}"    # env var injection
      tenant_id: "your-workspace-id"            # static workspace id (sent as X-Tenant-Id)

      # ── What to meter ────────────────────────
      metric_name_pattern: "{method} {path}"    # vars: {method} {path} {service} {route} {consumer}
      quantity_source: "1"                       # "1" (count), "response_size" (bytes), or a number
      customer_id_source: "consumer"             # Kong consumer identity (JWT claim wins when JWT is on)

      # ── Batching ─────────────────────────────
      flush_count: 50                            # events per flush
      flush_interval_ms: 5000                    # or flush every 5s

      # ── Plain HTTP events ────────────────────
      product_type: "API"                        # productType sent on non-protocol events

      # ── MCP (optional) ───────────────────────
      mcp_enabled: false
      mcp_product_id: ""                         # required when mcp_enabled: true

      # ── gRPC / GraphQL / WebSocket (optional) ─
      grpc_enabled: false                        # meters /pkg.Service/Method calls
      # grpc_path_prefix: "/grpc"                # strip a route prefix before parsing
      graphql_enabled: false                     # meters POSTs whose path contains graphql_path_pattern
      graphql_path_pattern: "graphql"
      websocket_enabled: false                   # one event per successful 101 handshake

      # ── Margin Guard (optional) ──────────────
      margin_guard_enabled: false
      margin_guard_url: ""
      margin_guard_cache_ttl: 30

      # ── Skip rules (optional) ────────────────
      exclude_paths: ["/health", "/metrics"]
      exclude_status_codes: [401, 403, 429]      # default — these produce no event

      # ── Outcome-Based pricing (optional) ─────
      # status_outcomes:                         # exact code -> executionStatus
      #   "404": "SUCCESS"
INFO
Only the three Core fields are required. Everything below has sensible defaults — flush_count 50, flush_interval_ms 5000, metric_name_pattern {method} {path}. Customer identity comes only from the Kong consumer (or a validated JWT claim), never a client-settable header.
INFO
Retries don't double-bill. Each event's idempotency key is built from the request id (X-Request-Id, or Kong's own X-Kong-Request-Id) — for WebSocket, the handshake key — so a batch the plugin re-sends after a failed flush is deduplicated by Aforo. HTTP, MCP, gRPC and GraphQL events also carry an executionStatus taken from the upstream status — see How Calls Get an Outcome.

Option B: Admin API

terminal
curl -X POST http://localhost:8001/plugins \
  -H "Content-Type: application/json" \
  -d '{
    "name": "aforo-metering",
    "config": {
      "aforo_endpoint": "https://api.aforo.ai/v1/ingest/batch",
      "api_key": "sk_live_your_key_here",
      "tenant_id": "your-workspace-id",
      "flush_count": 50,
      "flush_interval_ms": 5000
    }
  }'
INFO
The Admin API method applies the plugin globally (all routes). To scope to a specific service or route, add service.id or route.id to the request.

How Calls Get an Outcome#

Read this if any rate plan behind this gateway uses Outcome-Based pricing — it bills each call at a weight for its executionStatus. Every other pricing model ignores the field. The plugin sets it from the upstream response status:

Upstream statusexecutionStatus
2xx, 3xxSUCCESS
408, 504TIMEOUT
499 (client closed the request, where the gateway reports it)CANCELLED
400, 422VALIDATION_FAILED
401, 403, 429BLOCKED
Every other 4xx and 5xx (404 included)ERROR
Missing, 0, 1xxNot sent — the call bills at full weight
WARNING
A status with no weight on the rate plan bills at full weight (1.0). The rate plan wizard starts BLOCKED and CANCELLED at 0, but plans saved before those two statuses were added to the wizard (September 30, 2026) have no weight for them, so the gateway's BLOCKED and CANCELLED calls on those plans bill in full until you give them one — open the plan in the wizard and enter a weight (the fields show blank), or set it through the rate-plan API (outcomeWeights). See Outcome-Based pricing.

Override the status for specific codes

When the default table is wrong for your API — say a 404 is a normal answer, not a failure — map exact codes with the status_outcomes plugin config field, a map of status code to status:

kong.yml
plugins:
  - name: aforo-metering
    config:
      # ...core fields...
      status_outcomes:
        "404": "SUCCESS"      # a miss is a legitimate answer for this API
        "429": "ERROR"

Keys are exact HTTP codes from 200 to 599. Values must be one of the 11 statuses: SUCCESS, PARTIAL, TIMEOUT, ERROR, VALIDATION_FAILED, FAILED, FAILURE, CANCELLED, PENDING, BLOCKED, HITL_REQUIRED. Kong’s config schema rejects an invalid entry when you apply the plugin. If a code is listed twice, the last valid entry wins. An override can't give a status to a missing, 0 or 1xx code.

Requests that aren't metered

By default, responses with status 401, 403 or 429 and requests to /health, /ready or /metrics produce no event at all — they cost nothing whatever the rate plan says. All five gateway plugins share this default. A path is skipped when it equals an entry or starts with it, so /health also covers /health/live.

SettingDefaultTo meter everything
Plugin config exclude_status_codes (array)[401, 403, 429]An empty array, []
Plugin config exclude_paths (array)["/health", "/ready", "/metrics"]An empty array, []

Kong's config schema rejects an entry of the wrong type when you apply the plugin. A list you set replaces the default rather than adding to it: a status list of 404,500 skips only those two and starts metering 401, 403 and 429. A status you choose to meter bills as BLOCKED unless you override it above.

gRPC calls

gRPC returns HTTP 200 for most failures, so with grpc_enabled: true the plugin reads grpc-status instead — from the response header first, then the upstream trailer. Each code maps to an equivalent HTTP status, and that equivalent goes through the table, status_outcomes and exclude_status_codes above. By default that means codes 7, 8 and 16 produce no event, exactly like a 403, 429 or 401.

grpc-statusTreated asDefault executionStatus
0 OK200SUCCESS
1 CANCELLED499CANCELLED
3 INVALID_ARGUMENT, 9 FAILED_PRECONDITION, 11 OUT_OF_RANGE400VALIDATION_FAILED
4 DEADLINE_EXCEEDED504TIMEOUT
16 UNAUTHENTICATED401BLOCKED — not metered by default
7 PERMISSION_DENIED403BLOCKED — not metered by default
8 RESOURCE_EXHAUSTED429BLOCKED — not metered by default
5 NOT_FOUND404ERROR
6 ALREADY_EXISTS, 10 ABORTED409ERROR
12 UNIMPLEMENTED501ERROR
14 UNAVAILABLE503ERROR
2 UNKNOWN, 13 INTERNAL, 15 DATA_LOSS, any code above 16500ERROR
Not readableThe HTTP statusPer the HTTP table above

A gRPC-Web upstream that puts the status in the response body can't be read at the gateway; those calls fall back to the HTTP status. The Apigee, AWS, Azure and MuleSoft plugins don't meter gRPC.

Step 3: Deploy#

After configuration, reload Kong to activate the plugin:

terminal
# For traditional mode (database-backed)
kong reload

# For DB-less mode (declarative)
kong reload -c /etc/kong/kong.conf

# Verify the plugin is active
curl -s http://localhost:8001/plugins | jq '.data[] | select(.name == "aforo-metering")'

Performance: Zero Latency Impact#

The Aforo Kong plugin is designed for zero impact on your API response times:

<5ms
Entitlement Check
Local Redis lookup. No cross-network call.
0ms added
Usage Metering
Fires in Lua log_by phase — after response is sent.
Every 5s
Batch Flush
50 events per batch (flush_count). Async HTTP in the log phase.
30s TTL
Cache Refresh
Background sync. Stale cache serves until refresh.
<5ms
Kong Edge Decision
Local Redis lookup in access phase. Zero cross-network call.
MARGIN GUARD/ Kong
The plugin operates in Kong's log phase for metering and access phase for entitlement + margin checks. L1-L3 enforcement runs before the request reaches your upstream — unprofitable traffic is blocked at the gateway.

Fallback Behavior#

If the Aforo ingestion endpoint is unreachable, the plugin keeps your API traffic flowing and retries the batch. A batch that still fails is dropped and counted (aforo_metering_dropped_events_total), never retried forever:

RESILIENCE CHAIN
1. Normal → Events buffered in the aforo_buffer shared dict and flushed every 5s to api.aforo.ai
2. Ingest unreachable → The batch is retried with exponential backoff (1s, 2s, 4s)
3. Retries exhausted → The batch is dropped and counted (drop metric) rather than blocking traffic
4. Ingest recovered → Buffering + flush resume on the next request
INFO
Metering runs in Kong's log phase, after the response is sent — so a metering failure never blocks or delays an API request. Entitlement/margin checks in the access phase fail open (traffic continues) if Aforo can't be reached.

Verification#

Verify the handshake by checking Kong logs for a successful connection:

terminal
# Check Kong error log for Aforo handshake
tail -f /usr/local/kong/logs/error.log | grep aforo

# Expected output:
# [aforo-metering] Connected to api.aforo.ai (200 OK)
# [aforo-metering] Entitlement cache warmed: 47 tenants loaded
# [aforo-metering] Batch flush: 100 events sent (compressed: 2.4KB)

PaperPlaneTilt a Test Event

terminal
# Make an API call through Kong
curl -X GET http://localhost:8000/your-api/endpoint \
  -H "X-Tenant-Id: test_tenant_123" \
  -H "Authorization: Bearer sk_live_your_customer_key"

# Then confirm it arrived: in Aforo, open Intelligence → Usage Ingestion.
# The event shows up once the plugin has sent it (batching plugins flush every few seconds).
PRO TIP
If events are not appearing, check the Kong error log for [aforo-metering] ERROR entries. Common issues: invalid API key (401), network timeout to api.aforo.ai, or missing X-Tenant-Id header on the request.
Aforo Documentation — Aforo Docs