AI SWE Prep
0/9in System Design
System Design

APIs, Load Balancing & Rate Limiting

The front door: REST vs. gRPC, how a load balancer spreads traffic, and rate limiters that protect you from spikes.

~15 minLesson 51 of 60
Your progressNot started

The API is the front door of the system. It defines what clients can ask for, how failures are retried, how traffic spreads across instances, and how abuse is contained. A clean backend with a sloppy API still becomes an unreliable product.

Good API design is mostly about contracts: stable resource names, predictable pagination, safe retries, explicit limits, and load balancers that only send work to healthy capacity.

The mental model

Think of the request path as three contracts in a row:

client contract  ->  traffic contract  ->  fairness contract
API shape        ->  load balancer      ->  rate limiter
resources/errors ->  health/routing     ->  quotas/backpressure

The API tells clients how to behave. The load balancer decides where each request runs. The rate limiter decides whether the request is allowed to run at all.

REST vs. gRPC vs. GraphQL

Style Best fit Strengths Trade-offs
REST Public resource APIs, CRUD, broad clients Simple, cacheable, debuggable, works everywhere Can over/under-fetch; versioning needs discipline
gRPC Internal service-to-service calls Typed contracts, streaming, efficient binary protocol Harder for browsers and ad hoc debugging
GraphQL Product APIs with many client-shaped views Clients ask for exactly the fields they need Query cost control, caching, and auth get more complex

A common architecture uses REST at the public edge, gRPC internally, and GraphQL only where client flexibility is worth the operational cost.

Idempotency and pagination

Retries are inevitable. Networks fail after the server succeeds but before the client receives the response. Without idempotency, a retry can create two orders, two payments, or two jobs.

Make unsafe operations retry-safe with an idempotency key:

POST /orders
Idempotency-Key: user-123:create-cart-456

If the same key arrives again, return the original result instead of creating a
second order.

Pagination has the same contract problem. Offset pagination is easy but unstable under inserts: page 2 can skip or duplicate items when new rows arrive. Cursor pagination uses an opaque token tied to the last item seen.

GET /events?limit=50&cursor=eyJsYXN0X2lkIjoi...

The cursor should encode a stable sort key such as (created_at, id), not a page number.

Load balancing

A load balancer spreads traffic across healthy instances.

Layer Routes by Good at Example use
L4 IP and TCP/UDP connection Very fast, protocol-agnostic Raw TCP, high-throughput edge
L7 HTTP/gRPC request details Path/header routing, retries, auth hooks API gateways and service meshes

Common algorithms:

  • Round robin — simple rotation; good when requests cost about the same.
  • Least connections / least requests — better when request durations vary.
  • Weighted routing — send more traffic to larger instances or canaries.
  • Consistent hashing — keep the same key on the same backend, useful for sticky caches or long-lived sessions.

Health checks matter as much as the algorithm. A load balancer that routes to a stuck process is an outage amplifier.

Load balancer — spreading requests across serversStep 1 / 16
srv 10
srv 20
srv 30
srv 40
Strategy: round-robin. All servers idle — requests arrive from the left.
Round-robin is simple; least-connections adapts to uneven request costs; random is cheap but can clump.

Rate limiting

Rate limiting protects shared capacity. The main algorithms:

Algorithm How it behaves Fit
Fixed window Count requests per minute/hour Simple but bursty at boundaries
Sliding window Count requests over the last N seconds More accurate, more state
Token bucket Tokens refill over time; bursts spend saved tokens Best general-purpose API limiter
Leaky bucket Queue drains at a fixed rate Smooths traffic, can add latency

Token bucket is the interview default because it allows small bursts without letting a client exceed its long-term rate.

Pattern recognition

Variations

Worked problems

In API scenarios, define the client-visible behavior before jumping into servers.

ScenarioMedium
  • API design
  • Pagination
  • Idempotency

Design an idempotent paginated events API

Design an API for ingesting user events and listing them later. Clients may retry writes after timeouts. The list endpoint must paginate through new events without skipping or duplicating items.

Approach. Use an idempotency key for ingestion and cursor pagination for listing. Sort by a stable pair, not by page number.

Show solution

One possible contract:

POST /v1/events
Idempotency-Key: tenant-42:device-9:event-abc
Body: { "occurred_at": "2026-07-10T12:00:00Z", "type": "click", ... }

201 Created on first insert
200 OK with the same event id on retry with the same key
409 Conflict if the same key is reused with a different body

List endpoint:

GET /v1/events?tenant_id=42&limit=100&cursor=<opaque>
Response: { "items": [...], "next_cursor": "..." }

The cursor encodes the last (occurred_at, event_id) returned. The query asks for items after that pair in ascending order. event_id breaks ties so two events at the same timestamp do not duplicate or disappear across pages.

Storage needs a uniqueness constraint on (tenant_id, idempotency_key) and an index on (tenant_id, occurred_at, event_id). The API contract and database index are part of the same design.

ScenarioMedium
  • Load balancing
  • Health checks
  • WebSockets

Pick a load balancing strategy

A collaboration app serves normal REST requests, gRPC calls between internal services, and WebSocket sessions for live documents. Instances vary in size and WebSocket sessions can last for hours. What load balancing strategy do you use?

Approach. Separate request/response traffic from long-lived connections. Use L7 where request metadata matters and sticky or consistent routing where session locality matters.

Show solution

A reasonable split:

Traffic Strategy Why
Public REST L7 load balancer with weighted least requests Routes by path, respects canaries, handles varied request cost
Internal gRPC L7/service-mesh balancing with health checks Understands HTTP/2/gRPC and service identity
WebSockets Consistent hash or sticky routing by document/session id Keeps a live document’s connection state together

Weighted routing lets larger instances receive more traffic. Least requests is better than round robin when some endpoints are slow. For WebSockets, do not move a connection per request; route the initial upgrade to a healthy instance and keep it there.

Health checks should test dependency readiness, not just “process is alive.” If an instance cannot reach Redis or its document shard, the load balancer should stop sending it affected traffic before users discover the failure.

ScenarioHard
  • Rate limiting
  • Token bucket
  • Python

Implement token-bucket rate limiting

An API allows each user 100 requests/minute with bursts up to 20 requests. Sketch a token-bucket limiter for one user. What state do you store, and what happens when a request is rejected?

Approach. Store the current token count and last refill time. Refill based on elapsed time, cap at burst size, and spend one token per accepted request.

Show solution
import time

class TokenBucket:
    def __init__(self, rate_per_sec: float, burst: int):
        self.rate = rate_per_sec
        self.capacity = burst
        self.tokens = burst
        self.updated_at = time.monotonic()

    def allow(self) -> tuple[bool, float]:
        now = time.monotonic()
        elapsed = now - self.updated_at
        self.updated_at = now
        self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)

        if self.tokens >= 1:
            self.tokens -= 1
            return True, 0.0

        wait_seconds = (1 - self.tokens) / self.rate
        return False, wait_seconds

bucket = TokenBucket(rate_per_sec=100 / 60, burst=20)

For production, this state usually lives in a gateway or Redis keyed by user_id/API key. A rejected request should return 429 Too Many Requests with a Retry-After hint. You may also have separate buckets per endpoint so one expensive export API cannot starve cheap reads.

The burst size is 20, so a user can make 20 requests instantly after being idle. The refill rate is about 1.67 tokens/sec, which enforces the long-term 100/minute contract.

The classic pitfall

Your progressNot started