Rate limiting tutorials start with Redis because that is what planet-scale systems use. But a single-process tool calling a third-party API has a much simpler problem: do not exceed N requests per window. You can solve that in-process, for free.
The token bucket in twenty lines
A bucket holds tokens that refill at a fixed rate. Every request takes a token; when the bucket is empty you wait or reject. Bursts are absorbed up to capacity, the average rate stays capped — exactly the behavior APIs expect.
import time
class TokenBucket:
def __init__(self, rate: float, capacity: int):
self.rate, self.capacity = rate, capacity
self.tokens = capacity
self.updated = time.monotonic()
def allow(self) -> bool:
now = time.monotonic()
self.tokens = min(
self.capacity,
self.tokens + (now - self.updated) * self.rate,
)
self.updated = now
if self.tokens >= 1:
self.tokens -= 1
return True
return FalsePersisting state across restarts
An in-memory bucket forgets everything when the process restarts — and APIs count your usage regardless. Snapshot the counters to SQLite every few seconds and reload on boot: the limiter survives deployments without an external service.
def save(conn, key, tokens, updated):
conn.execute(
"INSERT OR REPLACE INTO buckets VALUES (?, ?, ?)",
(key, tokens, updated),
)
conn.commit()Concurrency gotchas
Guard the bucket with a lock if threads share it, and remember the limit is per process: two replicas each get their own budget. If you truly run a fleet, then — and only then — graduate to a shared store.
Simple, local, persisted. That is the whole pattern — and it is exactly what our RateShield asset packages with tests and configuration already done.