Auxiliary Systems
Rate Limiting & Abuse Prevention
Click a node to see what it does. Switch tiers above to see how the design scales.
Overview
A short-link creation endpoint is an obvious target for spam and phishing campaigns — one bad actor can mint thousands of malicious links a minute unless something stops them. This subsystem protects link creation first and foremost, with a much lighter touch on redirects, since a single popular link can legitimately produce enormous, non-abusive redirect volume.
API Design
Rate limiting isn’t its own resource — it’s middleware in front of
POST /api/shorten (and, more loosely, GET /{code}). It shows up in
response headers on every request, and as a distinct error once the
limit is hit.
POST /api/shorten
// 201 response headers
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1723459200
// 429 Too Many Requests
{
"error": "rate_limited",
"retry_after_seconds": 12
}
Returning limiter state on every response — not just the 429 — lets well-behaved clients back off before they’re ever rejected, which is the detail that separates “I added a rate limiter” from “I designed a rate-limited API.”
Database Schema
No durable table — limiter state is short-lived by design, living entirely in Redis, keyed by identity and endpoint:
ratelimit:{api_key}:{endpoint} → Hash { tokens, last_refill_ts }
HGETALL ratelimit:acme_9f2a:shorten
→ { "tokens": "12", "last_refill_ts": "1723459180" }
A Lua script performs the read-refill-decrement-write as one atomic
step — without atomicity, two concurrent requests could both read
tokens: 1, both decide they’re allowed through, and the limit gets
bypassed under exactly the concurrent load it’s meant to protect against.
Basic Approach — Fixed Window Counter Per IP
How it works
Keep a counter per client IP that increments per request and resets on a fixed schedule (e.g., once per minute). Reject once it exceeds the limit for the window.
Client request ──▶ API Gateway ──▶ increment counter[ip][current_minute] ──▶ over limit? 429 : allow
Tradeoffs
- Pro: Dead simple to implement and reason about — one counter, one comparison.
- Con: Bursty at window boundaries — a client can send the full limit at 11:00:59 and the full limit again at 11:01:00, doubling the effective rate in two seconds.
- Con: Limiting by raw IP punishes every user behind a shared IP (corporate NAT, mobile carrier) equally, whether they’re the abuser or not.
Scaled Approach — Token Bucket Per API Key
How it works
Each identity (API key, not raw IP) gets a bucket holding up to N tokens that refills continuously at a steady rate. Every request costs one token; an empty bucket means the request is rejected. Because it refills continuously rather than resetting at fixed boundaries, the window-burst problem disappears entirely.
Client request (with API key) ──▶ Rate Limiter ──▶ bucket has a token? consume it, allow : 429
(bucket refills continuously in the background)
Tradeoffs
- Pro: Naturally smooths traffic — no window-boundary exploit, while still allowing short legitimate bursts up to the bucket size.
- Pro: Keying by API key instead of IP means shared-IP users aren’t punished for one bad actor on the same network.
- Con: Requires every client to authenticate — a real product requirement a purely anonymous shortener doesn’t otherwise need.
- Con: Bucket state must live somewhere shared (Redis) across multiple API servers, or a client could exhaust a separate bucket per server and get N times the intended limit.
Advanced Approach — Tiered Limits + Anomaly Detection
How it works
Layer multiple limits at once: a per-second burst cap, a per-day quota, and a distinct, much stricter limit specifically on link creation versus the far higher-volume redirect path — since spam is a write-side risk, not a read-side one. On top of the bucket, a sliding-window anomaly detector watches for patterns like “500 links created in 10 seconds, all pointing at similar domains” and can flag or throttle an account before it ever hits a hard numeric limit.
Client request ──▶ Tiered Limiter (per-sec bucket AND per-day quota AND endpoint-specific limit)
└▶ Anomaly Detector (sliding window over recent creation patterns) ──▶ flag / throttle / allow
Tradeoffs
- Pro: Separating the shorten endpoint from the redirect endpoint means a legitimately popular link’s high redirect traffic is never penalized by a limit designed to catch spam link creation.
- Pro: Pattern-based detection catches abuse that stays technically under the numeric rate limit — many links, all just under threshold, all pointing at a known-bad domain.
- Con: Multiple simultaneous limits plus a pattern detector are meaningfully more infrastructure and tuning surface — false positives on legitimate bulk users become a real support cost.
- Con: Sliding-window computation over recent history is more expensive per request than a single counter check, so it typically runs asynchronously or sampled rather than synchronously on every request.
Tech Choices
- Token bucket — smooth, burst-tolerant limiting without fixed-window edge cases.
- Redis — shared, fast counter/bucket state across API servers.
- API Gateway — a natural place to enforce limits before a request even reaches application code.
How to Vocalize This in an Interview
Name the two very different things being protected — link creation (spam/phishing risk, low legitimate volume) and redirects (legitimate volume can be enormous for one popular link) — up front. Applying one limit to both is the most common mistake here, and naming the distinction unprompted signals you’ve actually thought about the failure mode.