Strings, hashes, lists, sets, and sorted sets
coreintermediateRedis is not a key-value store where every value is a blob — the value has a *type*, and the type decides which commands are available and what they cost. A **string** holds bytes and supports atomic `INCR`. A **hash** holds field-value pairs, so you can read or write one field without fetching the whole object. A **list** is an ordered sequence with cheap pushes and pops at either end, which is what makes it a queue. A **set** holds unique members with O(1) membership tests and set algebra. A **sorted set** gives every member a numeric score and keeps them ordered by it, which is how you build a leaderboard or a sliding-window rate limiter. Picking the right type is most of what "using Redis well" means.
Think of it as
The reason the type matters is that Redis is single-threaded for command execution, so every command you run is time nobody else's command gets. That turns type choice into a latency decision rather than a style preference. Storing an object as a JSON string means every field update is read-modify-write across the network, and two concurrent updaters lose one of their changes; storing it as a hash makes `HSET user:42 last_seen …` a single atomic field write with no read at all. The same reasoning explains why the collection types are worth learning rather than reaching for a string every time: `SISMEMBER` on a set is one round trip and constant time, while "fetch a JSON array and check in Python" is the whole array over the wire on every check. Sorted sets are the type most people under-use and the one that solves the most Django-shaped problems — a leaderboard is `ZREVRANGE`, a delayed queue is `ZRANGEBYSCORE` on a timestamp score, and a sliding-window rate limiter is `ZADD` plus `ZREMRANGEBYSCORE` plus `ZCARD`, all without a single scan.
What we're doing: A sliding-window rate limiter in a sorted set — exact, with no scan and no background cleanup job.
- 6
- Trimming by score *is* the expiry. Because the score is a timestamp, "older than the window" is a range delete rather than a scan over members.
- 7
- A unique member per attempt. Using the timestamp itself as the member would collapse two requests in the same instant into one recorded event.
- 9
- `EXPIRE` on the whole key means an idle user's data disappears on its own — no cleanup job, no growing keyspace.
- 5–10
- A pipeline sends all four commands in one round trip. Redis executes commands one at a time, so this is also close to atomic for a single key.
- 16
- The oldest score plus the window is exactly when a slot frees up — which is what a `Retry-After` header should say.
Why this works: A fixed-window counter in a string resets on a boundary and lets a caller send twice the limit across it. A sorted set keyed on time is a true sliding window, and trimming by score keeps it bounded without any periodic job.
Storing an object as a JSON string and updating one field
Wrong
Better
What you see: `last_seen` and `seats` occasionally revert to older values under concurrent traffic. Nothing errors, and it is unreproducible in a single-threaded test.
Why: A JSON string forces read-modify-write for every change, and the read and the write are separate commands with a gap between them. A second writer reading in that gap bases its write on stale data and overwrites the first change on `SET`. A hash makes the field the unit of update, so `HSET` writes only what changed and no read is involved at all.
- One Redis key
- String and Hash — a value, or an object
- INCR is atomic — a counter with no read-modify-write race
- HSET writes one field — no read, no lost update from a concurrent writer
- JSON-in-a-string loses both — every field change becomes read-modify-write
- List and Set — ordered, or unique
- LPUSH / BRPOP — O(1) at both ends, and BRPOP blocks — a work queue
- SISMEMBER is O(1) — membership without moving the collection to Python
- LRANGE 0 -1 is O(n) — a list is a queue, not something to scan repeatedly
- Sorted set — a score per member, kept in order
- ZREVRANGE 0 9 — a top-ten leaderboard, already sorted
- score = a timestamp — ZRANGEBYSCORE gives a sliding window or a delayed queue
- ZADD + ZREMRANGEBYSCORE + ZCARD — an exact sliding-window rate limiter, no scan
The five types, and the Django-shaped problem each one solves
Together
Remember: The value has a type, and the type decides both the commands and the cost. `INCR` on a string and `HSET` on a hash field are atomic single writes — JSON in a string turns every change into a lossy read-modify-write. Lists are O(1) at the ends and O(n) to scan, so they are queues. Sets give O(1) membership. Sorted sets are the one to reach for more often: score by time and you get sliding windows, delayed work, and leaderboards without a scan. And never `KEYS` in production — Redis is single-threaded, so it blocks everyone.
See also: expiry atomicity and messaging · redis roles in a django stack · cache backends keys and ttl

