"Redis: caches and distributed locks"

An in-memory data-structure server. What its single-threaded model gives you, the structures worth knowing, and how to use it for caching, rate limiting, leaderboards and locks without the usual traps.

On this page
  1. 01What it is and why it exists
  2. 02How it works, at the level you must explain
  3. 03When to use it, trade-offs, numbers
  4. Common mistakes
  5. Check yourself

Redis keeps data in memory and answers in well under a millisecond. In designs it shows up as a cache, a counter, a ranked list and a lock. It is more than a key-value store, and the structures are the reason to pick it.

Crux: use Redis when you need speed and a specific structure (a sorted set, an atomic counter, a lock with expiry). It is fast because it is in memory, so size it and decide what happens when it loses data.

What it is and why it exists

Redis is an in-memory data-structure server. A client sends a command such as INCR, ZADD or SET, and the server runs it on the structure stored under a key.

  • Command execution is single-threaded. Each command is atomic, with no locking inside Redis. That is what makes INCR and "set if absent" safe to use for counting and locking.
  • Because data sits in RAM, latency is dominated by the network round trip. A single node commonly serves on the order of tens to a hundred thousand operations per second for simple commands. Take that as a rough guide, not a promise.
  • Memory is the limit, and it is expensive compared to disk.

How it works, at the level you must explain

The structures worth knowing

stringcache value, counter, flag hasha small object listsimple queue setunique members sorted setleaderboard, top N geonear me HyperLogLogapproximate distinct count streamsmall log with groups pick the structure by the question you need answered fast
Pick the structure by the question you need answered quickly.
  • String: a value, or a number you can INCR. Caches, counters, flags.
  • Hash: fields inside one key. A small object.
  • List: push and pop at the ends. A simple queue.
  • Set: unique members, with fast "is this a member". Tags, unique visitors.
  • Sorted set: members ordered by a score. Leaderboards, "top N", and time-ordered feeds. Reads of a rank or a range are fast.
  • Geo: members with coordinates, with "within radius" search, built on a sorted set.
  • HyperLogLog: approximate count of distinct items in a tiny fixed space.
  • Streams: an append-only log with consumer groups. Lighter than Kafka, and it lives in memory.

As a cache

The usual shape is cache-aside: the app reads the cache, on a miss reads the database, then writes the cache with a TTL (time to live).

  • Keep keys small and values bounded.
  • Set a TTL on almost everything, so stale data fixes itself. Add a little random jitter, so many keys do not expire together.
  • Choose an eviction policy for when memory is full: LRU (least recently used) is the common one. LFU keeps frequently used items.
  • Guard against a cache stampede: when a hot key expires, many requests hit the database at once. Use a short lock so one request refills it, or refresh early.
  • Invalidation is the hard part. Deleting the key on write is simple. Decide how long stale data is acceptable.

As a rate limiter or counter

INCR a key per user per window and set a TTL on first use. If the count passes the limit, reject. Do it in a small script, or the SET and EXPIRE steps can race.

As a distributed lock

A lock that works across many app servers, used to hold a resource briefly (a seat, a job, a leader role).

  • Acquire with one atomic command: SET lock:key <unique-token> NX PX 30000 (set only if absent, expire in 30 seconds).
  • Release only if the token is still yours, with a small script that checks and deletes together. Otherwise you could delete someone else's lock after yours expired.
  • Always have the expiry, so a crashed holder cannot block everyone forever.
  • A lock can expire while its holder is still working (a long pause, for example). If correctness depends on it, pair it with a fencing token: a number that increases with each lock grant, which the protected resource checks and uses to refuse stale holders.
  • Redis replication is asynchronous, so after a failover two clients may both think they hold the lock. For a lock that must never be wrong, use a database transaction or a coordination service such as ZooKeeper. For efficiency-only locks ("avoid doing this twice"), Redis is fine.

Durability and scaling

  • Persistence is optional. A snapshot (RDB) saves periodically, an append-only file (AOF) logs every write. Both can lose a little recent data. Do not treat Redis as the only copy of data you cannot rebuild.
  • Replication copies a primary to replicas. Sentinel or a managed service promotes a replica on failure.
  • Redis Cluster splits keys across nodes by hash slot. Operations across keys on different nodes are limited, so related keys need a shared hash tag.

Rate limiting in more detail

  • Fixed window: one counter per user per window (INCR rate:user42:12:05, expire after the window). Simple and cheap. Weakness: a user can send a full quota at the end of one window and again at the start of the next, doubling the burst.
  • Sliding window with a sorted set: store each request time as a score, drop entries older than the window, count what is left. Accurate, but memory grows with the allowed rate.
  • Token bucket: a counter and a last-refill time per user, updated in a small script. Allows short bursts and a steady average.

Set the expiry once, when the key is created, not on every request. Resetting it each time can keep a key alive forever, and doubles the commands. Do the check and increment in one script so concurrent requests cannot slip past the limit.

Pub/sub, streams and Kafka

  • Pub/sub pushes a message to whoever is subscribed right now. If nobody is listening, or a subscriber is slow, the message is gone. It is at most once, with nothing stored. It fits live notifications where a miss is harmless.
  • Streams keep the entries, give each consumer group a position, and let a new consumer catch up. They are as durable as your persistence settings, and they live in memory.
  • Kafka is the choice for long retention, large volumes and replay over days.

Cluster routing and hot keys

Redis Cluster splits the key space into 16,384 hash slots spread over primaries. A client computes the slot for a key. If it asks the wrong node, the node replies with a redirect and the client retries. A multi-key command works only when all the keys are in one slot, so use a hash tag ({user42}:cart, {user42}:profile) to keep related keys together.

The key layout is the main scaling lever. Spread keys evenly, and watch for a hot key, such as a viral item, which sits on a single node whatever the cluster size.

  • For a read-hot key: cache it in the app process for a second, keep several copies under suffixed names and read a random one, or add replicas.
  • For a write-hot key (a global counter): replicas do not help. Split the key into several shards (counter:0 to counter:7), write to a random one, and add them up when reading.

Atomic scripts

A Lua script runs as one command, with nothing interleaved. Use it for check-and-update steps such as the lock release and the rate limiter above. Keep scripts short, as a slow one blocks everyone.

When to use it, trade-offs, numbers

  • What: a fast in-memory store of data structures.
  • Use when: caching, counters and rate limits, leaderboards, sessions, short-lived locks, lightweight pub/sub.
  • Know: cache-aside, TTL, eviction, stampede, atomic commands, lock with token and expiry, replication being asynchronous.
  • Careful: it is not your source of truth by default, memory is finite, and one hot key lives on one node.

Redis or something else?

  • Memcached: simpler, a pure cache, multi-threaded. Pick it for plain caching with no structures.
  • A local in-process cache: fastest, but each server has its own copy. Good for tiny, rarely changing data.
  • PostgreSQL: pick it for data you cannot lose. Use Redis in front of it.
  • ZooKeeper: pick it when lock correctness matters more than speed.

Numbers: I am leaving out memory pricing and per-instance benchmarks, because they vary by provider and over time. Do your own estimate from the data size: items times bytes per item, plus overhead.

Common mistakes

  • No TTL, so the cache grows and goes stale forever.
  • A lock with no expiry, or released without checking the token.
  • Using it as the only copy of important data.
  • One giant hot key that overloads a single node.
  • Saying "I'll cache it" without saying what the key is, the TTL, and how it is invalidated.
  • Resetting the TTL on every request in a rate limiter.
  • Expecting replicas to help a write-hot key.
  • Using pub/sub where a missed message matters.
  • Multi-key operations across keys with no shared hash tag.

Check yourself

  • Why is a single-threaded design useful for counters and locks?
  • Which structure gives you a leaderboard, and why?
  • Write out the steps of acquiring and releasing a lock safely.
  • What is a cache stampede and how do you reduce it?
  • Why is a Redis lock not enough for something that must never be double-booked?
  • When would you pick Memcached over Redis?
  • Why can a fixed-window limiter allow double the limit, and what fixes it?
  • How would you spread a write-hot counter?
  • When would you pick Kafka over Redis streams?