Redis Caching and Rate Limiting: A Practical Guide for Small Apps
Redis gets reached for reflexively, then used badly — cached values that never expire, rate limiters with race conditions, or both used to paper over a database query that should've just had an index. Here's the version that actually holds up in a small production app.
Pattern 1: Cache-aside, with a TTL you can defend
Read from cache; on a miss, read from the database, write to cache, return. The part people skip: every cached key needs a TTL you can explain out loud. "Cache forever" isn't a caching strategy, it's a bug waiting for the first time the underlying data changes and nobody remembers to invalidate it. For most small-app data (a rendered page fragment, an API response that doesn't change per-request), a TTL of minutes-to-an-hour beats a manual invalidation scheme you'll forget to call correctly.
Pattern 2: Rate limiting with a fixed window, done atomically
The naive version — read a counter, check it, increment it — has a race condition under concurrent requests. Use Redis's INCR (atomic by design) with EXPIRE set only on the first increment in a window:
INCR rate:user:123
if result == 1: EXPIRE rate:user:123 60
if result > limit: reject
This is a fixed-window limiter, not a sliding-window one — it allows a burst right at the window boundary. For most small apps that's an acceptable tradeoff for the simplicity; sliding-window log or token-bucket algorithms exist if you need smoother limiting, but they're rarely worth the added complexity at small scale.
Pattern 3: Session/ephemeral state, not long-term source of truth
Redis is excellent for things that are fine to lose — sessions, rate-limit counters, short-lived locks. It's a bad place to keep anything you'd be upset to lose on a restart or eviction, unless you've explicitly configured persistence (RDB/AOF) and understand the durability tradeoffs that come with it. Treat "is it okay if this disappears" as the test for whether something belongs in Redis at all.
The mistake that costs the most
Caching a query that was slow because it was missing a database index, instead of just adding the index. Caching hides the symptom and adds a new failure mode (stale data) without fixing the actual problem. Reach for the index first; reach for Redis when the data is genuinely expensive to compute or genuinely fine to be occasionally stale.
Takeaway
Redis is simple to add and easy to misuse. TTLs you can explain, atomic operations for anything concurrent, and honesty about what you're okay losing — that's most of what "using Redis well" actually means for a small app.