redis · diff

v2026.11.01 to v2026.11.04

2 added, 2 removed. Audit A to A.

---
name: redis
- description: "Designs and operates Redis data structures, caching, streams and clusters."
+ description: "Designs and operates Redis data structures, keys and TTLs, caching, locks, streams and clusters. Use when adding a cache, lock, rate limiter or queue on Redis, keys disappear or writes fail with OOM, a CROSSSLOT error appears, Stream consumers lose messages, or FT.SEARCH returns nothing, including a quick cache-aside helper with a TTL."
license: MIT (upstream attributions in NOTICE.md)
metadata:
author: HyperSkills
- version: "2026.11.01"
+ version: "2026.11.04"
category: framework
---
# redis
## Scope
Covers Redis the server and the code that talks to it: structure selection and the encodings
behind it, key naming, expiry and eviction, persistence and replication, transactions and
scripting, locks, Pub/Sub and Streams, Cluster, connections and pipelining, the cache patterns
that keep a database alive, the Redis Query Engine including vector search and semantic
caching, ACLs, and the commands that answer "what is this instance doing right now".
The body is written against **Redis 8.10** (current stable; 8.10.2 at time of writing, with
8.8.x, 8.6.x, 8.4.x, 8.2.x, 7.4.x, 7.2.x and 6.2.x still maintained). Rules that need a server
newer than 7.2 carry a gate such as `(7.4+)`. Confidence markers: `[verified]` measured here
against 8.10.1 (the cluster-bus rules against 8.10.2), `[official]` from Redis documentation,
`[community]` otherwise.
Not covered:
- Relational schema, indexes and query plans. Use the `postgres` skill.
- Document modelling, aggregation pipelines and Atlas Search. Use the `mongodb` skill.
- A full-text search platform's own index lifecycle, relevance tuning and cluster operations.
Use the `elasticsearch` skill.
- How a given framework wires a cache, a session store or a queue up: annotations, config
files, DI, ORM second-level caches, migration of cache code. Use the `java-spring`,
`laravel`, `fastapi` or `nodejs-backend` skill. This skill covers what those integrations
do *to Redis* — the key shapes they produce, the TTLs they set or fail to set, the commands
they issue per request.
- Application-level cache architecture: which layer should hold which data, whether to cache
at the CDN or in-process, cache hierarchies across services. That is a design question for
the owning service, not a Redis question.
- Memcached. Not covered — the eviction, expiry and data-structure model is different, and
nothing here transfers.
- Managed-hosting control planes: instance sizing and SKUs, provider backup and restore,
IAM-based auth, private endpoints, shard scaling through a provider API, Redis Cloud or
Redis Software administration, and vendor products layered on Redis. No skill in this
library covers those; say so rather than improvising provider commands. Everything at the
command and key-design level below still applies on a managed instance, but `CONFIG SET`,
`DEBUG`, `MODULE LOAD` and the config file usually will not.
## Core rules
1. Establish the server before quoting a gated rule: `INFO server` for `redis_version` and
`redis_mode`, `MODULE LIST` for the query engine and JSON. Hash-field TTLs need 7.4,
`FT.HYBRID` needs 8.4, vector sets need 8.0, and a `cluster` instance rejects multi-key
code that works standalone.
2. Command execution is single-threaded. Every O(N) command is latency for every other
client, which is why `KEYS`, `SMEMBERS`, `HGETALL`, `LRANGE key 0 -1`, `SORT` and
`SUNIONSTORE` over large containers are dangerous — the problem is head-of-line blocking,
not bandwidth.
3. Read the configuration from the instance, never from memory:
`CONFIG GET maxmemory maxmemory-policy appendonly save`. The stock values are `maxmemory 0`,
`noeviction`, `appendonly no`, `save 3600 1 300 100 60 10000` `[verified]` — an unbounded
instance whose only limit is the kernel OOM killer, with a loss window up to an hour.
4. Choose the structure from the access pattern: how you read it (whole / by field / by rank /
by membership), how it shrinks, whether entries expire individually, and whether two of
these keys must be touched atomically. Data shape alone never decides
(`references/data-structures.md`).
5. A serialised JSON document in a String makes every field update a read-modify-write of the
whole value, and makes concurrent updates lose writes. Use a Hash when fields change
independently; add `HEXPIRE`/`HGETEX` (7.4+) when individual fields must expire.
6. `OBJECT ENCODING` is the memory story, and the thresholds are per-instance config, not
folklore: on 8.10.1 the defaults are `hash-max-listpack-entries 512`,
`zset-max-listpack-entries 128`, `set-max-listpack-entries 128`, value limit 64 bytes
`[verified]`. Read them with `CONFIG GET hash-max-listpack-*` before claiming a structure
has been promoted to a hashtable.
7. One key lives in one slot and is served by one core, so a single huge key — a 60M-member
sorted set, a 2M-member set — is both a latency source and the reason one cluster shard is
four times the size of the others. Split by bucket or move it out.
8. A plain `SET` replaces the key *including its expiry*, so a session refresh written as
`SET key <new payload>` silently makes the key immortal. `KEEPTTL` preserves it; `APPEND`,
`SETRANGE`, `INCR`, `HSET` and `RENAME` preserve it; `GETSET` clears it `[verified]`.
9. Every cache key gets an explicit TTL at write time, and the TTL gets jitter. A batch warmed
with one constant TTL expires as a batch, which is how a warm-up job becomes a database
outage an hour later.
10. `volatile-lru` and its siblings only consider keys that have a TTL. With no such key the
instance answers writes with `OOM command not allowed when used memory > 'maxmemory'` —
byte-identical to `noeviction` `[verified]`. Pure cache: `allkeys-lru` or `allkeys-lfu`.
Source of truth: `noeviction` plus an alert. `volatile-*` only when both live in one
instance *and* every cache key has a TTL.
11. An expired key keeps its memory until something touches it or the active-expiry cycle
reaches it: with active expiry disabled, 300 keys stayed in `DBSIZE` a full second after
expiry and `used_memory` did not drop; one `EXISTS` freed one key, a full `SCAN` freed all
of them `[verified]`. `DBSIZE` counts keys that are already logically gone.
12. `SCAN` guarantees only that every element present for the whole iteration is returned at
least once; it may return duplicates and it may miss keys added mid-iteration. `COUNT` is
a hint per call, and `MATCH` filters *after* the bucket is read, so a selective pattern
over a large keyspace still costs a full walk.
13. On the miss path, concurrent requests for the same key all recompute it. Serialise them
with a single-flight lock (`SET lock:<key> <token> NX EX <n>`, the loser waits or serves
stale) or refresh ahead of expiry; a longer TTL does not fix it, it only moves it.
14. Requests for keys that will never exist bypass the cache entirely. Cache the negative
result under a short TTL with a sentinel distinguishable from a real payload, and put a
Bloom filter (`BF.RESERVE`/`BF.EXISTS`, built in on 8.0+) in front when the id space is
attacker-controlled.
15. Never invalidate by pattern. `KEYS product:*` blocks the server for the whole keyspace and
`SCAN` merely spreads the same work out. Maintain a reverse index of the keys a change
affects, or version the prefix so one write invalidates a whole class
(`references/caching-patterns.md`).
16. `MULTI`/`EXEC` and Lua give **isolation, not rollback**. An error while queuing (unknown
command, wrong arity) aborts the whole transaction with `EXECABORT`; an error at execution
time does not — `MULTI; INCR c; LPUSH str x; INCR c; EXEC` leaves `c` at 2, and a script
that fails halfway keeps the writes it already made `[verified]`.
17. A pipeline is not a transaction. It is one round trip for N independent commands, and any
of them can fail on its own. Reach for `WATCH` + `MULTI` for compare-and-set, or a script
when the read and the write must not be separated.
18. A Redis lock is a lease, not mutual exclusion. Store a unique token as the value, release
only via a compare-and-delete script or `FCALL`, and accept that a holder which overruns
the TTL loses the lock while still working. There is no fencing token, so anything whose
correctness depends on exclusivity needs the guard in the resource itself
(`references/transactions-and-scripting.md`).
19. Pub/Sub is at-most-once with no backlog: a subscriber that is not connected at publish
time never learns the message happened, and a slow one is disconnected on
`client-output-buffer-limit`. Anything that must survive a restart is a Stream.
20. Stream consumer groups are at-least-once. `XREADGROUP` hands an entry over and keeps it
pending until `XACK`, so the handler must be idempotent, a dead consumer's entries need
`XAUTOCLAIM`, and the stream needs `MAXLEN`/`XTRIM` or it grows forever.
21. In Cluster mode a key maps to one of 16384 slots by CRC16 of the key name, and every
multi-key command — `MGET`, `SDIFF`, `MULTI`, a pipeline sent as one unit, a script with
two `KEYS` — requires all keys in one slot. A hash tag `{...}` makes only the tagged
substring count; scope it to the entity (`user:{1001}:profile`), never to a bare id, and
only where a multi-key operation actually exists.
22. A cluster client must follow `MOVED` and `ASK` and refresh its slot map; pointing a
single-node client at one cluster node works until the first resharding. Replica reads are
opt-in and asynchronous, so read-your-own-writes breaks — fine for leaderboards and price
lookups, wrong for state a request just wrote.
23. Share connections: a pool that leases per call, or a single multiplexed connection. Never
one connection per request. Blocking commands (`BLPOP`, `BLMOVE`, `XREAD BLOCK`) need
their own connection, because on a multiplexed one they stall every other caller. Set both
a connect timeout and a command timeout, connect the shorter of the two.
24. Out of the box the `default` user is `on nopass ~* +@all` `[verified]` — full access with
no password. Give each application its own ACL user with the key patterns and command
categories it needs, and remove capability by category (`-@dangerous`, `-@admin`) rather
than by renaming commands, which breaks clients and cluster tooling silently
(`references/security.md`).
25. Finish by measuring, and quote the numbers: `DBSIZE`, `INFO memory` (`used_memory`,
`mem_fragmentation_ratio`, `evicted_keys`, `expired_keys`), `INFO stats`
(`keyspace_hits`/`keyspace_misses`, `rejected_connections`), `SLOWLOG GET`, and
`redis-cli --bigkeys` before and after. "It should be faster now" is not a result.
Paths below are relative to this skill's directory.
## Workflows
### choose-structures-and-design-the-key-space
- [ ] Write down the read patterns first — by id, by field, by rank, by membership, by time
range — and the write patterns beside them. Then pick per pattern, not per entity
(`references/data-structures.md`).
- [ ] Reject the two defaults people reach for without thinking: a serialised blob in a String
(read-modify-write, lost updates) and a List named "queue" (no acknowledgement).
- [ ] Fix the naming: lowercase, colon-separated, `service:entity:id:attribute`, a tenant
segment in front when the instance is shared, and no URLs or free text as keys
(`references/keyspace-and-expiry.md`).
- [ ] Decide the expiry policy per key class at design time: which keys have a TTL, whether it
slides, and what must happen when one is evicted early.
- [ ] Size it: estimate members per key and bytes per member, check the resulting encoding
against the instance's listpack thresholds, and split anything heading past a few
thousand members or a megabyte.
- [ ] Note every place two keys must change together — that is either a script, or a hash tag
if the instance is or may become a cluster.
- [ ] **Gate — measured on a real instance:** load a representative sample, then confirm
`MEMORY USAGE <key>`, `OBJECT ENCODING <key>` and `DEBUG SLEEP`-free `SLOWLOG` are what
the design assumed. Quote the per-key bytes and the projected total.
### fix-a-cache-that-is-hurting-the-database
- [ ] Classify the symptom before changing code: a periodic spike is synchronised expiry, a
spike on one id is a hot-key stampede, a flood of misses on ids that do not exist is
penetration, and a permanent regression is usually eviction or a missing TTL
(`references/caching-patterns.md`).
- [ ] Check the eviction policy against the key population: `CONFIG GET maxmemory
maxmemory-policy` plus `INFO stats` for `evicted_keys` and `expired_keys`. A
`volatile-*` policy with unexpired keys is the whole bug.
- [ ] Add jitter to every batch-written TTL, and refresh hot keys ahead of expiry rather than
on the miss.
- [ ] Put a single-flight lock on the recompute path and decide explicitly what the losers
do — wait with a bounded retry, or serve the stale value.
- [ ] Cache negative lookups with a sentinel and a short TTL; add a Bloom filter if the id
space is unbounded.
- [ ] Replace pattern-based invalidation with a reverse index or a versioned prefix, and make
every write path that mutates the source also touch the cache.
- [ ] Audit the refresh writes for dropped expiry: any `SET` that is meant to update a value
but keep the TTL needs `KEEPTTL` or an explicit re-`EXPIRE`.
- [ ] **Gate — the source load actually fell:** re-run the scenario and quote the before/after
of `keyspace_hits`/`keyspace_misses`, the query count on the source, and the p99 of the
endpoint. A hit-ratio number is only meaningful for the cache-aside prefixes —
`INFO stats` is instance-wide and includes session, lock and queue traffic.
### diagnose-memory-eviction-or-latency
- [ ] Separate "slow" from "blocked" first: `INFO clients` for `blocked_clients`,
`CLIENT LIST` for a connection stuck in a blocking read, `INFO stats` for
`rejected_connections` against `maxclients` (`references/operations.md`).
- [ ] Get the slow commands rather than guessing: `SLOWLOG GET 25`. The threshold is
`slowlog-log-slower-than`, 10000 microseconds by default, and the log holds
`slowlog-max-len` 128 entries `[verified]` — read it before it rolls.
- [ ] Turn on latency monitoring if the answer is not in `SLOWLOG`:
`latency-monitor-threshold` defaults to 0, meaning `LATENCY DOCTOR` and
`LATENCY HISTORY` have nothing to report until you set it `[verified]`.
- [ ] Attribute memory: `INFO memory` for `used_memory` versus `maxmemory` and
`mem_fragmentation_ratio`, `redis-cli --bigkeys` for the shape of the keyspace,
`MEMORY USAGE` on the suspects. `MEMORY DOCTOR` declines to answer on a small instance,
so do not treat its silence as health `[verified]`.
- [ ] Decide whether the growth is data, expired-but-untouched keys, client output buffers or
fragmentation — each has a different fix, and only the first is solved by a bigger box.
- [ ] Check the command mix with `INFO commandstats` and `INFO latencystats`: one O(N) command
at low rate explains more p99 than a million `GET`s.
- [ ] **Gate — the number moved:** re-run the load and quote `used_memory`, `evicted_keys`,
the `SLOWLOG` entry count above threshold, and the p99 from `--latency-history`.
### move-code-onto-a-cluster
- [ ] Inventory every multi-key operation in the code: `MGET`/`MSET`, set and zset store
commands, `MULTI` blocks, scripts with more than one key, and any pipeline whose
commands are assumed to run together (`references/cluster-and-connections.md`).
- [ ] For each one, decide between a hash tag and decomposition. Tag when the keys genuinely
belong to one entity; decompose when the grouping was incidental — tagging everything
rebuilds a single-node bottleneck inside the cluster.
- [ ] Choose the tag scope deliberately: the entity, not the raw id, so unrelated namespaces
do not collide on one slot.
- [ ] Swap the client for a cluster-aware one and confirm it splits pipelines per node and
follows `MOVED`/`ASK`; a single-node client aimed at one node is a latent outage.
- [ ] Find the keys that cannot shard — one enormous sorted set, set or stream — and split or
relocate them before blaming the shard balance.
- [ ] Decide replica reads per call site, not globally, and list the call sites that must stay
on the primary because they read what they just wrote.
- [ ] **Gate — proven on a cluster:** run the suite against a real 3-primary cluster (not a
single node), confirm zero `CROSSSLOT` errors, and check distribution with
`CLUSTER KEYSLOT` on the tagged keys plus `DBSIZE` per node.
### build-a-queue-that-does-not-lose-messages
- [ ] Decide the delivery guarantee out loud. At-most-once is a List; at-least-once needs a
Stream with a consumer group; exactly-once does not exist and has to become
at-least-once plus an idempotent handler (`references/streams-and-messaging.md`).
- [ ] For at-least-once, lay out the four commands together: `XADD` to produce, `XREADGROUP`
to claim, `XACK` after the side effect succeeded, `XAUTOCLAIM` to recover a dead
consumer's pending entries.
- [ ] Make the handler idempotent before wiring the ack: a `SET done:<id> 1 NX EX <n>` guard
or an idempotency key on the downstream call.
- [ ] Bound the stream: `XADD ... MAXLEN ~ <n>` or a periodic `XTRIM MINID`, and decide what
happens to entries nobody ever acked.
- [ ] Give consumers their own connections — a blocking read on a shared multiplexed
connection stalls unrelated traffic.
- [ ] If a List has to stay, make it `BLMOVE` from the queue into a per-consumer processing
list and add the reaper that returns abandoned items.
- [ ] **Gate — kill a consumer mid-work:** send `SIGKILL` to a consumer between claim and ack,
then show the entry in `XPENDING` and show `XAUTOCLAIM` handing it to another consumer
and the work completing exactly once from the outside.
### design-or-debug-a-search-or-vector-index
- [ ] Confirm the engine exists and its version: `MODULE LIST` for `search`, `ReJSON`,
`vectorset`. On 8.x the official image ships them; on 7.x it does not
(`references/search-and-vectors.md`).
- [ ] Match the field type to the query, not the value's Java type. An exact-match value in a
`TEXT` field is tokenised and stemmed, so `@sku:ABC-123` returns **zero** results
`[verified]` — `TAG` is the type for identifiers, statuses and enums.
- [ ] Use `FT.EXPLAIN` on any query that returns nothing before touching the schema; it shows
how the query was parsed, which is usually the whole answer.
- [ ] For vectors, make `DIM`, `DISTANCE_METRIC` and `TYPE` follow the embedding model
exactly; a mismatch produces plausible garbage rather than an error. Choose the
mechanism deliberately: an RQE `VECTOR` field when the query also filters on attributes
or full text, a vector set (`VADD`/`VSIM`) for a standalone KNN collection — its default
`quant-type` is `int8`, which is lossy `[verified]`.
- [ ] Filter before the vector comparison in the same query rather than over-fetching and
filtering client-side.
- [ ] Change schemas behind an alias (`FT.ALIASUPDATE`) so queries never point at an index
being rebuilt.
- [ ] **Gate — profiled, not assumed:** run `FT.PROFILE ... SEARCH QUERY "..."` on the real
query and quote the returned counters, plus a recall spot-check against a brute-force
`FLAT` result for the vector case.
### review-a-deployment-config
- [ ] Read the four settings that decide whether the instance survives contact with
production: `maxmemory`, `maxmemory-policy`, `appendonly`, `save`. Say what each stock
value costs here (`references/operations.md`).
- [ ] State the durability window explicitly. RDB-only with the default save points loses up
to an hour; AOF with `appendfsync everysec` loses about a second; `always` costs
throughput. Then say whether that is acceptable for *this* data, and whether the data
exists anywhere else.
- [ ] Check the replication and failover story: `INFO replication` for the role and lag, and
whether anything actually promotes a replica. A replica alone is not high availability.
- [ ] Audit access: `ACL LIST`, whether `default` still has `nopass`, whether TLS is on, what
`bind` and `protected-mode` are, and which commands the application user can reach. On a
cluster, the bus port has no authentication unless `tls-cluster yes`; check that and
`cluster-bus-port-protected-mode` (`references/security.md`).
- [ ] Check the connection ceiling: `maxclients` against pool size times instance count, and
`INFO stats` for `rejected_connections`.
- [ ] Confirm observability exists before the incident: metrics exported from `INFO`, a
`slowlog-log-slower-than` that matches the latency budget, and
`latency-monitor-threshold` set to something other than 0.
- [ ] **Gate — the failure was rehearsed:** kill the primary on a staging copy and record what
the application does, how long the gap is, and what data is missing afterwards.
## Topic router
| Topic | Read when | File |
|---|---|---|
| Structure selection per access pattern, Hash versus JSON, encodings and the measured listpack thresholds, big-key detection and splitting, Bitmap, HyperLogLog, geospatial, Bloom, Stream and vector-set storage, memory accounting | Deciding how to store something, or auditing what is already stored | `references/data-structures.md` |
| Key naming and namespaces, the full TTL-preservation table, `EXPIRE` family and hash-field TTLs, lazy versus active expiry, every `maxmemory-policy` and its failure mode, LRU/LFU sampling, keyspace notifications | Keys vanish, keys never vanish, or writes start failing with OOM | `references/keyspace-and-expiry.md` |
| Cache-aside and write paths, TTL jitter, single-flight and stale-while-revalidate, negative caching and Bloom filters, versioned-prefix and reverse-index invalidation, client-side caching, rate limiting, semantic caching for LLM responses | Building or repairing a cache in front of another system | `references/caching-patterns.md` |
| `MULTI`/`WATCH` semantics, pipelines versus transactions, Lua versus `FUNCTION`, script replication and determinism, lock leases with token release, why Redlock does not give what people think | Two operations must not interleave, or a lock is misbehaving | `references/transactions-and-scripting.md` |
| Pub/Sub delivery and output-buffer limits, keyspace notifications as an event source, Stream anatomy, consumer groups, `XPENDING`/`XAUTOCLAIM` recovery, trimming, List queue patterns and their limits | Messages are lost, duplicated or piling up | `references/streams-and-messaging.md` |
| Slot mapping and hash tags, the full `CROSSSLOT` surface, `MOVED`/`ASK` and slot-map refresh, resharding, connection pooling versus multiplexing, pipelining, timeouts and retries, replica reads | Moving to Cluster, or a client is the bottleneck | `references/cluster-and-connections.md` |
| RDB save points and AOF fsync policies with their loss windows, rewrite behaviour, replication and failover, `WAIT`, the `INFO` fields worth alerting on, `SLOWLOG`, `LATENCY`, `MEMORY`, `--bigkeys`, `--latency-history` | Deciding durability, or answering "what is it doing right now" | `references/operations.md` |
| ACL users, selectors and command categories, the `default` user's stock permissions, TLS, `bind` and `protected-mode`, cluster bus authentication and its real port, why `rename-command` is the fallback, script and injection surface, audit checklist | Hardening an instance or reviewing one against a finding | `references/security.md` |
| `FT.CREATE` schema design, field types and the exact-match trap, query-syntax and tokenisation gotchas, `FT.EXPLAIN`/`FT.PROFILE`, vector fields versus vector sets, HNSW parameters, hybrid retrieval, index aliases | Working on a Redis Query Engine index or vector search | `references/search-and-vectors.md` |
## Output format
When reviewing Redis usage — code, `redis.conf`, or both — group findings by file and lead each
with the location and the blast radius:
```
worker.py:22 - data-loss - RPOP removes the job before ship() succeeds, so a worker killed
mid-flight drops the order. Replace with XREADGROUP + XACK after ship(), and XAUTOCLAIM to
recover a dead consumer's pending entries.
redis.conf:4 - outage - maxmemory-policy volatile-lru with no TTL on any key behaves exactly
like noeviction: writes fail with OOM. Set allkeys-lru for a pure cache.
```
Order findings by blast radius: data loss first, then availability (OOM, blocking commands,
connection exhaustion), then correctness (lost updates, stale reads), then cost and style. For
a performance finding, quote the evidence — a `SLOWLOG` entry, a `MEMORY USAGE`, an
`OBJECT ENCODING`, a `--bigkeys` line — rather than asserting that something is slow, and give
the before/after command.
## Environment
- Reproduce against the same major version as production. `docker run -d --name r
redis:8-alpine` is enough to settle expiry, eviction, encoding, transaction and cluster-slot
questions, and running the question beats quoting a blog post. Add
`redis-server --enable-debug-command yes` when a test needs `DEBUG`, which is refused by
default.
- `MODULE LIST` tells you whether the query engine, JSON, Bloom and time-series are present.
The 8.x official image bundles them; older images and self-compiled servers do not.
- `redis-cli` carries the diagnostics worth knowing: `--bigkeys`, `--memkeys`, `--hotkeys`
(needs an LFU policy), `--latency-history`, `--stat`, and `--cluster check`.
- `DEBUG`, `CONFIG SET`, `MODULE LOAD` and `FLUSHALL` are commonly disabled or ACL-blocked on
managed instances. Check `ACL WHOAMI` and `ACL GETUSER <user>` before proposing a command
the application's credentials cannot run.