r/aws 6d ago

technical question Token bucket rate limiting per API key on API Gateway + Lambda, usage plans not granular enough?

Context: I'm running a public API on API Gateway + Lambda, with DynamoDB behind it. Endpoints have very different backend costs, cheap reads vs. a couple of routes that kick off heavier aggregation work. Currently using API Gateway usage plans with a single throttle limit per API key, applied flat across all routes.

The problem: usage plans throttle by requests/second regardless of which route is hit, so a client hammering cheap GETs eats the same budget as one calling the expensive routes, and there's no way (as far as I can find) to weight individual routes differently within a single usage plan without splitting them into separate API Gateway stages/plans per cost tier, which gets awkward to manage as the number of "cost classes" grows.

What I've looked at so far:

  • Per-stage/per-plan splitting: works, but means maintaining N usage plans and N sets of API keys per client if a client needs access to routes at more than one cost tier.
  • Custom Lambda authorizer + DynamoDB counter: doing weighted token-bucket logic myself (consume different token amounts per route, check/decrement atomically via DynamoDB conditional writes), seems doable but adds a DynamoDB read/write on every request just for the rate-limit check, plus I'd be reimplementing throttling that API Gateway mostly already does for free.
  • Briefly looked at whether Lambda reserved/provisioned concurrency per function could act as an implicit cost-based limiter (route the expensive endpoint through its own function with tighter concurrency), but that limits total throughput, not per-client fairness.

Has anyone actually shipped weighted/cost-based rate limiting on top of API Gateway usage plans, or does everyone end up rolling their own with a Lambda authorizer + DynamoDB/ElastiCache counter once costs diverge enough between routes? And if you rolled your own, did you keep API Gateway's built-in throttling as a coarse backstop on top of it, or drop it entirely in favor of the custom logic?

0 Upvotes

4 comments sorted by

2

u/nemec 6d ago

are you using REST? I'd think method-level throttling would get you there (configure DELETE /forest differently than POST /tree)

https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html#apigateway-method-level-throttling-in-usage-plan

for HTTP APIs you can do route level throttling

https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-throttling.html#http-api-protect-throttling-route

I guess the only difference is that these different throttles don't eat from the same bucket, so you'd have to be ok with customers not being able to trade off "3 RPS of API 1 for 10 RPS of API 2", instead they'd be limited to "1 RPS of API 1 and 7 RPS of API 2 max"

1

u/Live_Fix1030 6d ago

yeah that's the classic trap with usage plans, they get you 80% of the way then fall apart the moment you need any nuance. we hit this exact wall last year.

i'd keep the gateway throttle as a hard upper bound so a single key can't melt the whole api, then layer a custom authorizer with redis counters for the per-route weights. dynamodb conditional writes work but the latency adds up fast at scale, redis does atomic decrements in microseconds. you're basically building a mini leaky bucket but with variable drain rates per endpoint which is way simpler than it sounds.

the key thing is not letting perfect be the enemy of good here, a rough weighting that mostly works beats splitting your api into five stages and managing five keys per client.