Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

API Gateway API-key management + rate-limited workload

A small, deployable example of native API Gateway API-key management with throttle and quota (usage plans) on the classic REST API.

A client calls the workload endpoint with an x-api-key header. A separate management Lambda exposes unauthenticated CRUD over API keys and usage-plan tiers, persists metadata in DynamoDB, and reconciles every change to API Gateway via boto3. DynamoDB is the source of truth; API Gateway enforces throttle and quota — there is no rate-limiting in the application code.

⚠️ Demo only. The /admin/** routes have no API key and no authorizer on purpose, so you can exercise them easily from the command line. Do not deploy this as-is to anything reachable by untrusted clients — add an authorizer or IAM auth to /admin/** first.

What this deploys

Management (open — demo only)                Workload (x-api-key required)
  POST/GET  /admin/keys                        GET /workload   (private: true)
  GET/PATCH/DELETE /admin/keys/{keyId}             |
  GET  /admin/plans                                | x-api-key + usage plan
  GET/PUT  /admin/plans/{tier}                     v
        |                                    API Gateway REST API  --throttle/quota-->  workload Lambda
        v
  apiKeyManager Lambda  --boto3-->  API Gateway (API Keys + Usage Plans)
        |
        +---- DynamoDB (ApiKeysTable) ---->  KEY + PLAN metadata (source of truth)
  • apiKeyManager — handles all /admin/** routes, manages API keys and usage plans in API Gateway, and stores metadata/tier config in DynamoDB.
  • workload — trivial echo handler behind GET /workload, which requires an API key. API Gateway applies the usage-plan throttle/quota before the Lambda runs.
  • ApiKeysTable — single DynamoDB table holding both API-key metadata and usage-plan (tier) config.

Prerequisites

  • An AWS account with credentials configured locally and permission to create CloudFormation, Lambda, API Gateway (REST), IAM, and DynamoDB resources.
  • Node.js and npm, used to run the Serverless Framework v3 (osls).

Version note. serverless.yml pins frameworkVersion: "3". The bare npx osls command now resolves to osls 4.x, a major version that conflicts with v3 — so every command below pins osls@3 explicitly (e.g. npx osls@3 deploy).

  • Python 3.12 — the deployed Lambda runtime. boto3 ships in the AWS Python runtime.
  • AWS Lambda Powertools is supplied to every function at runtime by its public Lambda Layer (attached in serverless.yml via an SSM dynamic reference, so it always tracks the latest python3.12 x86_64 version). It is installed locally as a dev-only dependency through uv sync for editor type hints; nothing is bundled into the deployment package.
  • AWS CLI is recommended for inspecting resources directly.

Confirm the credentials and region are the ones you expect:

aws sts get-caller-identity
aws configure get region

The service deploys to us-east-1 by default. Set the AWS CLI profile before the commands below when needed:

export AWS_PROFILE=my-profile

Quick start

  1. Install the Python development environment (type stubs only, optional but recommended):

    uv sync
  2. Deploy the stack:

    npx osls@3 deploy
  3. Get the deployed REST API endpoint:

    npx osls@3 info --verbose

    Copy the endpoint for GET - /workload (or any admin route) and assign its base URL:

    export API_URL="https://YOUR_API_ID.execute-api.us-east-1.amazonaws.com"
  4. Create an API key and exercise the workload (see Smoke tests).

How it works

DynamoDB schema

One table (ApiKeysTable) with a partition key pk and a entityTypeIndex GSI on entityType for list queries.

Entity pk entityType Other attributes
API key KEY#{keyId} KEY keyId, name, status (active/inactive), tier, usagePlanId, createdAt, updatedAt
Usage plan / tier PLAN#{tier} PLAN tier, usagePlanId, rateLimit, burstLimit, quotaLimit, quotaPeriod (DAY/WEEK/MONTH), createdAt, updatedAt

The raw API key value is never stored — it is returned once, on creation, and is not recoverable afterward. List/get responses contain only metadata.

Usage plans

Two default tiers are seeded lazily (on the first admin call, or the first key created for that tier) and applied to API Gateway:

Tier rate (req/s) burst quota
free 1 2 100 / DAY
pro 10 20 10000 / DAY

Usage plans are owned by the apiKeyManager Lambda, not by CloudFormation, so DynamoDB stays the source of truth and there is no stack drift when you change limits at runtime via PUT /admin/plans/{tier}. The trade-off: npx osls@3 remove does not delete the API keys/usage plans created at runtime — remove them via the API or the console when finished.

The four-link enforcement chain

For the workload method to be throttled/quotad, all four must hold at once:

  1. The method has apiKeyRequired: true — set by private: true on the workload event.
  2. The presented API key exists and is enabled (a disabled key returns 403, not 429).
  3. That key is associated with a usage plan (create_usage_plan_key).
  4. That usage plan references this API's stage (apiStages) and defines a throttle and/or quota.

The management Lambda establishes links 2–4 as it creates keys and plans.

Response codes from API Gateway

Situation Status
Missing / invalid / disabled key on /workload 403 Forbidden
Throttle exceeded (rate/burst) 429 with x-amzn-ErrorType: ThrottledException
Quota exhausted 429 with x-amzn-ErrorType: TooManyRequestsException

HTTP API surface

Management (open — demo only):

Method & path Body Purpose
POST /admin/keys { "name": "...", "tier": "free"|"pro" } Create a key; returns value once
GET /admin/keys List key metadata (no values)
GET /admin/keys/{keyId} Get one key
PATCH /admin/keys/{keyId} { "name"?, "status"?, "tier"? } Rename, enable/disable (active/inactive), or change tier
DELETE /admin/keys/{keyId} Delete the key
GET /admin/plans List tier configs (self-heals the defaults)
GET /admin/plans/{tier} Get one tier config
PUT /admin/plans/{tier} { "rateLimit", "burstLimit", "quotaLimit", "quotaPeriod" } Create/update a tier in DynamoDB and API Gateway

Workload (API key required):

Method & path Purpose
GET /workload Echo {"ok": true, "message": "workload"} (if the key is valid and within limits)

Smoke tests

Create a key, then exercise the workload. The first authenticated /workload may return 403 for ~10–30 seconds after key creation while API Gateway's key cache propagates the association — retry after a short pause.

export API_URL="https://YOUR_API_ID.execute-api.us-east-1.amazonaws.com"

# 1. Create a key for the free tier. Save the returned value — it is shown once.
RESP=$(curl -s -X POST "$API_URL/admin/keys" \
  -H 'Content-Type: application/json' \
  -d '{"name":"demo","tier":"free"}')
echo "$RESP"
KEY=$(echo "$RESP" | python3 -c 'import sys,json; print(json.load(sys.stdin)["value"])')
KEY_ID=$(echo "$RESP" | python3 -c 'import sys,json; print(json.load(sys.stdin)["keyId"])')

# 2. Call /workload without a key -> 403 (rejected at API Gateway, no Lambda invocation).
curl -s -o /dev/null -w "%{http_code}\n" "$API_URL/workload"   # 403

# 3. Call /workload with the key -> 200. (Wait ~10-30s after step 1 if you get 403.)
curl -s -H "x-api-key: $KEY" "$API_URL/workload"               # {"ok": true, "message": "workload"}

Observe throttle and quota (free tier: burst 2, quota 100/DAY):

# Throttle 429: a parallel burst far exceeds burst=2. Expect a mix of 200 and 429.
for i in $(seq 1 10); do
  curl -s -o /dev/null -w "%{http_code}\n" -H "x-api-key: $KEY" "$API_URL/workload" &
done; wait

# Quota 429: fire more than 100 requests sequentially; later ones return 429.
for i in $(seq 1 101); do
  curl -s -o /dev/null -w "%{http_code}\n" -H "x-api-key: $KEY" "$API_URL/workload"
done

Key and plan management:

# List/get/update/delete keys.
curl -s "$API_URL/admin/keys"
curl -s "$API_URL/admin/keys/$KEY_ID"
curl -s -X PATCH "$API_URL/admin/keys/$KEY_ID" -H 'Content-Type: application/json' -d '{"status":"inactive"}'
curl -s -X DELETE "$API_URL/admin/keys/$KEY_ID"

# Plans: list, then change the free tier limits (propagates to the API Gateway usage plan).
curl -s "$API_URL/admin/plans"
curl -s -X PUT "$API_URL/admin/plans/free" -H 'Content-Type: application/json' \
  -d '{"rateLimit":2,"burstLimit":5,"quotaLimit":1000,"quotaPeriod":"DAY"}'

Useful commands

# Show deployed functions, endpoint, and resources
npx osls@3 info --verbose

# Follow function logs while sending requests
npx osls@3 logs --function apiKeyManager --tail
npx osls@3 logs --function workload --tail

# Package without deploying
npx osls@3 package

# Remove the stack created by this service
npx osls@3 remove

Project layout

serverless.yml            Service, runtime, Powertools layer, packaging, includes
functions/
  index.yml               Function definitions, per-function IAM, REST http events
  api_key_manager.py      CRUD + usage-plan reconciliation + route dispatch
  workload.py             Echo handler behind an API-key-required method
resources/
  dynamodb.yml            ApiKeysTable (PK-only + entityTypeIndex GSI)
utils/helper.py           API Gateway proxy response + body parsing helpers
pyproject.toml            uv project; dev-only deps (Powertools, boto3, boto3-stubs)

Caveats

  • Propagation delay. A freshly created/associated key can take ~10–30s (occasionally up to ~2 min) to take effect; a deleted/disabled key can linger in the cache for up to ~5 min.
  • Raw key value. Returned only at creation; not persisted, not recoverable.
  • Usage plans are Lambda-owned. npx osls@3 remove deletes the stack (table, functions, REST API) but orphans runtime-created API keys and usage plans — remove them via the API or the console. This is intentional so DynamoDB remains the source of truth and there is no stack drift on redeploys.
  • Non-transactional key creation. If associating a key with its usage plan fails after the key is created, the handler best-effort deletes the orphan key and returns an error.
  • Admin routes are open. As called out above, this is for local learning/demo only.

Cleanup

Remove the deployed stack when you are finished to avoid ongoing charges:

npx osls@3 remove

Then remove any API keys/usage plans created at runtime via DELETE /admin/keys/{keyId} (before removing the stack) or the AWS console.

About

A small, deployable example of **native API Gateway API-key management** with **throttle and quota (usage plans)** on the classic REST API.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages