Serverless distributed rate limiter using the token bucket algorithm on DynamoDB, Lambda, and API Gateway — deployed with AWS CDK. TypeScript, ESM, Node 22, with atomic conditional writes and optimistic locking for correctness under concurrency.
Topics: rate-limiter · token-bucket · dynamodb · aws-lambda · api-gateway · aws-cdk · serverless · typescript · optimistic-locking · distributed-systems · nodejs
A per-client token bucket rate limiter built on serverless AWS primitives: API Gateway (HTTP API) → Lambda → DynamoDB, deployed with AWS CDK v2.
It bursts up to capacity, settles to a steady rate, stays correct under concurrent requests across Lambda instances (optimistic locking), cleans up idle clients via DynamoDB TTL, and fails open so the limiter can never take down the API it protects.
Client ──GET /check (x-api-key)──> API Gateway HTTP API
│ Lambda proxy integration
▼
Lambda (token-bucket logic)
│ GetItem / PutItem / UpdateItem
▼
DynamoDB RateLimitTable
One DynamoDB item per client holds tokens, lastRefill, version, expiresAt.
| Path | Purpose |
|---|---|
bin/app.ts |
CDK app entry point |
lib/rate-limiter-stack.ts |
Infrastructure: table, Lambda, HTTP API, IAM |
src/config.ts |
Tunables (capacity, rate, TTL, retries) read from env |
src/bucket.ts |
Pure token-bucket math — no AWS, fully unit-tested |
src/store.ts |
DynamoDB read/write with optimistic locking + retry |
src/handler.ts |
Lambda entry: identify client, format HTTP response |
test/bucket.test.ts |
Unit tests for the pure logic (node:test) |
- Node.js 22+ (
node --version) - AWS credentials configured (
aws configureor SSO) - CDK bootstrapped once per account/region:
npx cdk bootstrap
npm install
# run the unit tests (no AWS needed)
npm test
# preview the infrastructure changes
npx cdk diff
# deploy
npm run deployOn success CDK prints the endpoint:
RateLimiterStack.ApiUrl = https://xxxxxxxx.execute-api.<region>.amazonaws.com/check
API="https://xxxxxxxx.execute-api.<region>.amazonaws.com/check"
# single request — allowed
curl -i -H "x-api-key: acme" "$API"
# drain the bucket — first 10 succeed (200), the rest are rejected (429)
for i in $(seq 1 15); do
curl -s -o /dev/null -w "%{http_code} " -H "x-api-key: acme" "$API"
done; echo
# prove concurrency safety — exactly 10 get through
seq 1 50 | xargs -P 50 -I{} curl -s -o /dev/null -w "%{http_code}\n" \
-H "x-api-key: stress" "$API" | sort | uniq -cResponses carry X-RateLimit-Limit, X-RateLimit-Remaining, and (on 429)
Retry-After.
All tunables are environment variables with defaults (see src/config.ts):
| Variable | Default | Meaning |
|---|---|---|
BUCKET_CAPACITY |
10 |
Max tokens (burst size) |
REFILL_RATE |
1 |
Tokens added per second (steady rate) |
TTL_SECONDS |
3600 |
Idle-bucket lifetime before TTL cleanup |
MAX_RETRIES |
5 |
Optimistic-lock retries before failing open |
TABLE_NAME |
RateLimitTable |
Injected by CDK at deploy time |
npm run destroyremovalPolicy is DESTROY in this demo so the table is removed with the stack.
Switch it to RETAIN before using this for anything real.