API Rate Limiting: Protecting Your SaaS from Abuse
Token bucket, sliding window, or leaky bucket — when to use which, plus concrete Redis implementations for small-to-mid SaaS.
Why rate limiting is mandatory
Without rate limits, one buggy or abusive client can:
- Overload the database — queries at 10x normal volume.
- Spike cloud costs — egress, compute, log storage.
- Hurt other users — server slow for everyone.
For SaaS, rate limiting isn’t optional. Default it on every endpoint.
3 popular algorithms
1. Token Bucket
- User has a “bucket” of N tokens. Refills at rate R per second.
- Each request consumes 1 token. Empty → 429.
- Best for: general APIs, allow small bursts while capping the average.
2. Sliding Window
- Count requests in a recent window (e.g. 60 seconds). If > limit → 429.
- Best for: strict anti-abuse with accurate rate prediction.
- Downside: per-request storage (timestamps).
3. Leaky Bucket
- Queue requests, drain at a constant rate. Queue full → 429.
- Best for: smoothing bursts (e.g. webhook delivery).
For standard SaaS, Token Bucket is the most flexible default.
Redis implementation (Token Bucket)
-- KEYS[1]: rate-limit key
-- ARGV[1]: capacity (max tokens)
-- ARGV[2]: refill rate per second
-- ARGV[3]: now (ms timestamp)
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local last = redis.call("HMGET", KEYS[1], "tokens", "ts")
local tokens = tonumber(last[1]) or capacity
local ts = tonumber(last[2]) or now
local elapsed = (now - ts) / 1000
tokens = math.min(capacity, tokens + elapsed * rate)
if tokens < 1 then
redis.call("HMSET", KEYS[1], "tokens", tokens, "ts", now)
return 0 -- denied
else
tokens = tokens - 1
redis.call("HMSET", KEYS[1], "tokens", tokens, "ts", now)
redis.call("EXPIRE", KEYS[1], 60)
return 1 -- allowed
end
Run with EVAL — atomic, race-free.
Tier-based limits (multi-tier SaaS)
tiers:
free:
requests_per_min: 15
burst: 30
basic:
requests_per_min: 60
burst: 100
pro:
requests_per_min: 240
burst: 500
On each request → look up the user’s tier → load the limit → enforce via token bucket.
Good response headers
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1736812800
Retry-After: 23
Clients back off correctly without guessing.
Best practices
- Limit at the edge (Cloudflare Workers, API gateway) — keep noisy traffic out of app servers.
- Limit per IP + per API key — both. IP alone is bypassable with rotating addresses.
- Soft limit (warning) before hard limit (block) — give clients a chance to fix.
- Whitelist internal cron/batch jobs.
SaaS architecture consult for deeper rate-limiting + architecture chats.