Free lab Real Python 3. Zero installs. Your code stays in this browser. Open the playground

API Rate Limiter & Retry Engine

Build the traffic control every public API runs: a token bucket, a per-user sliding window, exponential backoff for flaky calls, and middleware that answers 429 with an honest Retry-After.

Advanced 4 steps 170 XP ~60 min

How it works in the real world

Every serious API (Stripe, GitHub, the Claude API) protects itself from overload, and every serious client copes with being told to slow down:

  1. Token bucket: a bucket refills at a steady rate and each request spends a token, allowing short bursts but a fixed long-run rate.
  2. Sliding window: count each user's requests in the last N seconds for fair per-user limits.
  3. Backoff: when a call fails, wait 1s, 2s, 4s... before retrying, capped, so a struggling server gets room to recover.
  4. Middleware: put it in front of every request and reply 429 Too Many Requests with a Retry-After header.

Time is passed in explicitly rather than read from a clock, which is also how you would make this code testable in production.

Build progress0 / 4 steps
1

Token bucket

Finish TokenBucket: it starts full (capacity tokens), refills rate tokens per second up to capacity, and allow(now) spends one token if available. Print t=TIME ALLOW or t=TIME DENY for each request time.

Blank · autosaved
PYstep_1.py
Run your code to check this step…
2

Per-user sliding window

Limit each user to LIMIT requests in any WINDOW-second span. Keep a deque of timestamps per user, drop the ones older than the window, and print USER@TIME ok or USER@TIME limited. One user's traffic must never block another.

Blank · autosaved
PYstep_2.py
Run your code to check this step…
3

Retry with exponential backoff

Write call_with_retry(fn, retries=5, base=1, cap=8): on ConnectionError, wait min(cap, base * 2 ** attempt) seconds (recorded, not slept) and try again; any other exception is not retried. Print every attempt, then the result and total wait. Run it against a call that fails three times and one that always fails.

Blank · autosaved
PYstep_3.py
Run your code to check this step…
4

Rate-limit middleware

Put a token bucket per API key in front of handler. Unknown keys get 401. A key out of tokens gets 429 with Retry-After set to the whole seconds until one token is back (math.ceil). Print one line per request.

Blank · autosaved
PYstep_4.py
Run your code to check this step…

Take it further

Run it in front of a real web app, then make it work across several servers. Nothing below is graded: it runs on your own machine, not in this tab.

Stretch goals

  1. Wrap a Flask or FastAPI app with this middleware and hammer it with a loop of requests.get calls.
  2. Store bucket state in Redis so every server instance shares the same limits.
  3. Add random jitter to the backoff (random.uniform(0, delay)) and explain why it stops many clients retrying in lockstep.
  4. Read the Retry-After header in your client and wait exactly that long instead of guessing.

middleware_flask.py: the step 4 limiter in a real Flask app

# pip install flask
import math
import time

from flask import Flask, jsonify, request

app = Flask(__name__)
PLANS = {"key-free": (2, 0.5), "key-pro": (5, 5)}
buckets = {}  # key -> [tokens, last_time]


@app.before_request
def rate_limit():
    key = request.headers.get("X-API-Key", "")
    if key not in PLANS:
        return jsonify(error="unknown API key"), 401
    capacity, rate = PLANS[key]
    tokens, last = buckets.get(key, [capacity, time.monotonic()])
    now = time.monotonic()
    tokens = min(capacity, tokens + (now - last) * rate)
    if tokens < 1:
        buckets[key] = [tokens, now]
        wait = math.ceil((1 - tokens) / rate)
        return jsonify(error="slow down"), 429, {"Retry-After": str(wait)}
    buckets[key] = [tokens - 1, now]


@app.get("/data")
def data():
    return jsonify(ok=True)


if __name__ == "__main__":
    app.run(port=8000)

Ask an AI to review your build

I implemented a token bucket, a per-user sliding window limiter, exponential backoff and a Flask middleware that returns 429 with Retry-After. Here is my code: [paste it]. Where would it break under real load (multiple processes, clock changes, memory growth per key)? Explain the trade-offs between token bucket and sliding window for a public API.

Your next ten minutes

Write Python that does something useful.

Start free. No install, no card, no passive video marathon.

Start learning free → Explore the path