Day 3: Rate-limit checkout before bots do

Scale ShopStream: System Design in 7 Builds (Python) ~6 min read 35 min hands-on

A scalper bot takes 995 of 1,000 units. Stop it with a per-user token bucket in Redis (atomic Lua) β€” shoppers get 25 of 25 orders.

A limited product drops: 1,000 units. Five real shoppers want one each. A scalper runs a script. Ten seconds later the scalper has 995 units and the shoppers have five. In this lesson you reproduce that, then put a per-user token bucket in Redis in front of checkout. Same ten seconds, same bot: the shoppers get every order they try for and 960 units are still on the shelf.

The problem

ShopStream's checkout is correct β€” the conditional UPDATE … WHERE stock >= qty from lesson 1 never oversells. But correct isn't fair. A client that sends orders as fast as the network allows will always beat a human who clicks once. And the same pattern hurts any shared resource: one noisy tenant, one retry loop gone wrong, one buggy mobile release can use up capacity everyone else needs.

Rate limiting answers a narrow question: how many requests may this caller make in this period? Choosing the caller (user, API key, IP, device) and the shape of the limit is the design work.

What you build

  • ratelimit.py β€” a token bucket per user, kept in a Redis hash rl:<user_id> with two fields: tokens and ts (last refill time).

  • app.py β€” POST /orders asks ratelimit.allow(user_id) first. If refused, it answers 429 Too Many Requests with a Retry-After header and never touches the database. It also turns SQLite's "database is locked" under heavy write load into a 503, so clients get an answer instead of a dropped connection.

  • botdemo.py β€” 20 bot threads ordering product 42 as fast as they can, and 5 shoppers ordering once every 2 seconds, for 10 seconds.

The token bucket

Each user has a bucket that holds up to 5 tokens and refills at 1 token per second. An order costs one token. A full bucket lets a user place 5 orders back to back (a burst), and after that one per second β€” far more than any human checks out, far less than a script wants.

The refill isn't a background job. Each request computes it: tokens = min(capacity, tokens + elapsed Γ— rate). Idle users cost nothing; their key expires with PEXPIRE.

Why it's a Lua script

The obvious Python version is: read the bucket, compute, write it back. Run that from 20 threads and two of them read "1 token" at the same moment, both decide yes, both write 0. The bot gets twice its quota. That's a race between the read and the write.

Read the full lesson free

This lesson is one of the first 3 in its course, free with an account β€” the full article, implementation guide and scripts. No card needed.

Already a member? Sign in