upstash/ratelimit misfiring: algorithm, identifier granularity, and dynamic limits
A limiter that blocks too much or too little is usually the wrong algorithm, a cold cache, or dynamic limits fighting the config.
Diagnosing a misfiring @upstash/ratelimit
Symptom A: legitimate users get 429s
- Check the algorithm. Fixed window counts hard resets at window
boundaries: two requests straddling the boundary both count fully. Sliding window smooths this.
- Check the identifier. A constant identifier like "api" applies one
global limit to all users. One abusive client burns everyones quota. Use per-user or per-IP identifiers.
- Check dynamic limits. If enabled, a stale dynamic limit override
can sit far below your intended rate. Read it back with getDynamicLimit.
Symptom B: abuse gets through
- Fixed window leaks bursts at boundaries by design. If bursts are
your threat, switch to sliding window or token bucket.
- The runtime cache only stores identifiers AFTER they were rate
limited once. On serverless cold starts the cache is empty, so the first burst of every cold instance always hits Redis. That is a latency cost, not a bypass: Redis still enforces.
- Multi-region without waitUntil on pending: regions diverge and each
enforces only its local view.
Step 3: confirm with getRemaining
getRemaining(identifier) shows the current quota state without consuming. Compare it against what you configured. If the numbers do not match your config, the limiter instance in production differs from the one you think you deployed.
Fix and verify
Align algorithm, identifier granularity, and dynamic limits with the threat. Then burst-test: send 2x the limit in a tight loop and confirm roughly the expected fraction gets 429s.
Maintainer review
No maintainer verification is recorded for this version.
This records the version a maintainer checked. It does not assert that the version is the latest upstream release.