pr walkthrough
Per-token rate limits
Moves rate limiting from per-IP to per-token, so one office NAT can't exhaust the budget for every client behind it. Three files, one new bucket table, no behavior change for untokened requests.
Shape of the change
- gateway/
- limits.py+38-9
- middleware.py+6-2
- migrations/
- 0042_token_buckets.sql+11
The interesting part is
gateway/limits.py:41: the bucket key changes from the remote address to the token id when one is
present. Everything else is plumbing that key through.
The ceilings themselves
None of these numbers move. What moves is what they are counted against: a plan's ceiling used to be spent by every client behind one address, and is now spent per token.
| Plan | A minute | Burst | Counted against |
|---|---|---|---|
| Free | 60 | 120 | the token |
| Team | 600 | 1,200 | the token |
| Enterprise | 6,000 | 12,000 | the token, per environment |
| Untokened | 60 | 60 | the remote address, as before |
The core diff
gateway/limits.py+4 −2
@@ -38,9 +38,11 @@ class Limiter: - def bucket_key(self, request): - return request.remote_addr + def bucket_key(self, request): + if request.token: + return f"tok:{request.token.id}" + return f"ip:{request.remote_addr}" def allow(self, request): key = self.bucket_key(request) return self.buckets[key].take()
Why the prefixes matter
def bucket_key(self, request): if request.token: return f"tok:{request.token.id}"return f"ip:{request.remote_addr}"Unprefixed, a token id that happens to look like an IP would share a bucket with that address. The prefixes make the two namespaces disjoint.
Testing
- Unit: bucket key for tokened, untokened, and both-present requests.
- Integration: two tokens behind one address each get a full budget; two addresses on one token share one.
Both suites run under the rate-limit marker:
# the integration half needs the migration applied first
cd gateway && alembic upgrade head
pytest tests/ -m ratelimit --maxfail=1