Design Instagram
A social media platform for short-form video content.
A server-side rate limiting system that protects APIs from abuse by controlling request volume using sliding window counters and Redis-backed distributed state.
Rough, order-of-magnitude numbers. The point is to justify the architecture, not to be exact.
A diagram shows what exists; a trace shows what happens. Press play on a flow, or click any step, and the diagram above lights up the exact path that request takes.
A user sends an API request that falls within their allowed quota. The rate limiter checks, approves, and forwards it to the backend.
A user or attacker exceeds their rate limit. The middleware rejects the request before it ever reaches the backend.
Every box on the canvas, and the job it does. Click a card to locate it in the diagram; click the node itself for the full deep dive.
The rate limiter sits as middleware between the API Gateway and backend services. Every inbound request passes through it, where a lightweight decision engine checks the caller's current usage against configured rules before deciding to allow, throttle, or block.
The design supports multiple algorithms, configurable per-route:
In a multi-datacenter deployment, all rate limiter instances share the same Redis cluster. Cross-region replication keeps counters eventually consistent — slight over-counting during synchronization windows is acceptable compared to the latency cost of synchronous coordination. Local in-process counters serve as a fallback during Redis degradation, providing approximate rate limiting until the cluster recovers.
An architecture is a record of trade-offs. For every major choice here: what won, what lost, and why the constraints made it so.
Client-side rate limiting is trivially bypassed by modifying client code or disabling JavaScript. Server-side enforcement at the API gateway guarantees every request — regardless of origin — passes through the same gate. This is the only placement that can protect against both malicious attackers and buggy clients.
Rate limiting demands sub-millisecond reads and writes on the hot path of every API request. Redis delivers this with in-memory storage and atomic ops like INCR and ZREMRANGEBYSCORE. A relational database adds 10–50ms of query latency per check, and its durability guarantees are unnecessary because lost counters only mean a temporary over-allocation. In-process counters can't scale across instances without a shared store.
Fixed window counters suffer from burst-at-boundary spikes — 100 requests at 0:59 and 100 more at 1:00 double the effective rate. Token buckets need careful burst tuning. Sliding window log uses Redis sorted sets (ZSET) to store timestamps and evict expired entries in O(log N), providing precise per-window accounting. The memory cost is acceptable: at 100 req/user with 5M active users, that's roughly 5 GB of timestamp data.
Embedding the rate limiter as middleware avoids an extra network hop on the critical path — every request would need two hops instead of one if the limiter were a separate service. For a component that must decide on every single request, that latency penalty is unacceptable. A standalone service only makes sense when the rate limiter must be shared across completely independent API gateways.
Hard blocking is the simplest and most predictable strategy for API protection. HTTP 429 with Retry-After tells the client exactly what happened and when to retry. Throttling requires buffering and queuing, which adds complexity and can mask capacity problems. Shaping requires priority scheduling across the entire backend — too invasive for most systems.
Reading a system means sensing where it cracks under 10× load. These are the pressure points of this design, and how it holds.
A viral user or a botnet sharing a single IP generates millions of requests, concentrating all counter reads and writes on one Redis shard. That shard becomes a hot spot, latency spikes, and the rate limiter itself starts timing out.
Shard the keyspace by user ID or a composite key (user_id + endpoint) so load distributes evenly across Redis cluster nodes. For the hottest keys, use local in-process approximate counters that sync to Redis asynchronously — short-term precision loss is acceptable.
In a multi-instance deployment, two middleware processes can read the same counter before either writes, both see the count below threshold, and both allow the request through — letting a burst slip past the limit.
Use Redis atomic operations: INCR for fixed-window counters (atomic increment and check), or Lua scripting for sliding window sorted sets (evict, count, add, and check in one atomic script execution). These guarantee correctness without distributed locks.
Sliding window and fixed window algorithms rely on accurate timestamps. If middleware instances in different data centers have clock drift of even a few seconds, the counters may systematically over-count or under-count requests across regions.
Use Redis server timestamps via the TIME command instead of the middleware's local clock. For sliding window algorithms, align window boundaries to a server-coordinated epoch rather than local wall-clock time.
A sliding window log stores every individual request timestamp in a Redis sorted set. At 100 requests per second per user with 10M active users in a 1-minute window, that's 60 billion entries — infeasible for any reasonable Redis cluster.
Switch to the sliding window counter algorithm for high-volume routes: it stores only two integers per window (the previous window's count and the current window's count), reducing memory from O(N) to O(1) per identifier. Reserve sliding window logs for low-volume, high-precision routes like payment APIs.
If Redis becomes slow or partially unavailable, every middleware instance may accumulate pending Redis commands. The backlog increases latency for the rate limiter itself, which can cascade into timeouts across all upstream API calls.
Implement a circuit breaker: if Redis P99 latency exceeds 10ms, fall back to approximate local counters that are slightly over-generous (e.g., 110% of the configured limit). This sacrifices precision for availability. Redis Sentinel provides automatic failover for primary nodes.
If you can answer these without scrolling back up, the architecture is yours. Try each one out loud before revealing.
Every great system starts as a sketch on a whiteboard. The ability to zoom out and see the whole picture (how services connect, where data flows, what breaks and why) is what separates engineers who build features from engineers who build systems. This diagram is more than boxes and arrows. It's a map of decisions, trade-offs, and intentional design.
Studying architectures isn't just about passing interviews. It's about training your intuition. The more systems you take apart, the better you get at sensing where a monolith will crack, where a queue belongs, or when a cache is hiding a deeper problem. You start seeing patterns instead of chaos.
So keep reading, keep tracing those edges, keep asking "why this way and not that way." The engineers who truly understand large systems are the ones who never stop being curious about how things fit together. That curiosity is the only ingredient that really matters.
Atharva Arbat@arbat_atharva
A social media platform for short-form video content.

A microblogging platform with tweet ingestion, hybrid fan-out timelines, typeahead search, and real-time notifications.
A scalable, highly available cloud file storage and synchronization service.