The Complete Redis Guide — Features, Best Practices & Patterns
A comprehensive reference for Redis data structures, architecture, and production best practices.
A comprehensive reference for Redis data structures, architecture, operational patterns, and production best practices.
Table of Contents
- Introduction & Core Concepts
- Data Types Deep Dive
- Expiration & Eviction
- Persistence: RDB & AOF
- Replication
- Redis Sentinel (High Availability)
- Redis Cluster (Sharding)
- Transactions & Scripting
- Pub/Sub & Streams
- Common Design Patterns
- Caching Strategies
- Performance & Memory Optimization
- Security Best Practices
- Monitoring & Observability
- Anti-Patterns to Avoid
- Client Libraries & Connection Pooling
- Backup, Disaster Recovery & Upgrades
1. Introduction & Core Concepts
Redis (REmote DIctionary Server) is an in-memory data structure store used as a database, cache, message broker, and streaming engine. Key architectural facts every engineer should internalize:
- Single-threaded execution model for command processing (I/O threading exists since Redis 6 for network reads/writes, but command execution itself is still single-threaded). This means Redis commands are effectively atomic relative to each other — no two commands run in parallel on the same instance.
- In-memory first: all active data lives in RAM; persistence to disk is optional and asynchronous by default.
- Rich data types, not just key-value strings — this is what separates Redis from Memcached.
- Extremely low latency: sub-millisecond for most operations because there’s no disk I/O in the hot path.
Why single-threadedness matters
Because only one command executes at a time:
- Long-running commands (e.g.,
KEYS *, unboundedSORT, hugeSMEMBERS) block everything else, including health checks — this is a leading cause of production incidents. - You never need application-side locking for a single Redis operation, but you DO need to be careful when doing multi-step “read-modify-write” logic (see Transactions section).
2. Data Types Deep Dive
String
The most basic type; can hold text, serialized JSON, or binary data up to 512MB.
SET user:1000:name "Ayşe"
GET user:1000:name
INCR page:views # atomic counter
INCRBY wallet:1000 500
SETEX session:abc123 3600 "token-data" # TTL in one call
SETNX lock:job:42 "worker-1" # only set if not exists (naive locking)
Best practice: Use SET key value EX seconds NX instead of separate SETNX + EXPIRE — the combined form is atomic; doing it as two calls creates a race condition window.
SET lock:job:42 "worker-1" EX 30 NX
Hash
Ideal for representing objects (like a row/document) without needing multiple keys.
HSET user:1000 name "Ayşe" email "ayse@example.com" age 29
HGET user:1000 name
HGETALL user:1000
HINCRBY user:1000 login_count 1
HDEL user:1000 age
Best practice: Prefer one hash per entity over many flat string keys (user:1000:name, user:1000:email, …). Hashes are far more memory-efficient for small objects due to internal listpack encoding, and let you fetch/update the whole entity atomically.
List
Ordered collection, implemented as a linked list of quicklist nodes. Great for queues, timelines, recent-activity feeds.
LPUSH queue:jobs "job1"
RPUSH queue:jobs "job2"
LPOP queue:jobs
BRPOP queue:jobs 5 # blocking pop with 5s timeout — basis of simple job queues
LRANGE timeline:user:1000 0 20 # latest 20 items
LTRIM timeline:user:1000 0 999 # cap list size (keep only latest 1000)
Best practice: Always LTRIM capped lists (e.g., activity feeds) to prevent unbounded memory growth.
Set
Unordered unique collection. Excellent for tags, unique visitor tracking, relationship graphs.
SADD tags:post:55 "redis" "database" "cache"
SISMEMBER tags:post:55 "redis"
SINTER online:users vip:users # intersection: online VIP users
SUNIONSTORE all:tags tags:post:55 tags:post:56
SCARD tags:post:55 # count
Sorted Set (ZSet)
Unique members ordered by a floating-point score. One of Redis’s most powerful structures — backs leaderboards, rate limiters, priority queues, delayed job scheduling.
ZADD leaderboard 1500 "player1" 2200 "player2"
ZINCRBY leaderboard 50 "player1"
ZREVRANGE leaderboard 0 9 WITHSCORES # top 10
ZRANK leaderboard "player1"
ZRANGEBYSCORE leaderboard 1000 2000
ZREMRANGEBYRANK leaderboard 0 -1001 # keep top 1000, trim rest
Pattern — time-ordered events: use a timestamp as the score to get a naturally sorted event log, then ZRANGEBYSCORE for range queries by time window.
Bitmaps
Strings treated as bit arrays — extremely memory-efficient for boolean flags at scale (e.g., daily active user tracking).
SETBIT active:2026-08-17 1000 1 # user 1000 was active today
BITCOUNT active:2026-08-17 # daily active users
BITOP AND result active:mon active:tue # active both days
HyperLogLog
Probabilistic structure for approximate cardinality (unique counts) using only ~12KB regardless of set size, with ~0.81% standard error.
PFADD unique:visitors:2026-08-17 "user1" "user2" "user3"
PFCOUNT unique:visitors:2026-08-17
PFMERGE unique:visitors:month unique:visitors:2026-08-17 unique:visitors:2026-08-18
Use when: you need approximate unique counts (page views, distinct search terms) at massive scale and can’t afford a full Set’s memory cost.
Geospatial
Built on sorted sets internally; stores lat/lng and supports radius/box queries.
GEOADD shops -0.1276 51.5072 "shop:london"
GEOSEARCH shops FROMLONLAT -0.12 51.50 BYRADIUS 5 km ASC
GEODIST shops shop:london shop:paris km
Streams
An append-only log data type (since Redis 5) modeled after Kafka-style logs — the right choice for event sourcing, activity logs, and multi-consumer pipelines (covered more in Section 9).
XADD events:orders '*' order_id 1001 status "created"
XRANGE events:orders - +
XREAD COUNT 10 STREAMS events:orders 0
3. Expiration & Eviction
TTL Mechanics
EXPIRE session:abc 3600
PEXPIRE session:abc 3600000 # milliseconds
TTL session:abc # seconds remaining, -1 = no TTL, -2 = doesn't exist
PERSIST session:abc # remove TTL
Redis uses two eviction mechanisms for expired keys:
- Passive: checked lazily when the key is accessed.
- Active: a background cycle randomly samples keys with TTLs and removes expired ones, running periodically to bound memory held by stale data even if never accessed again.
Eviction Policies (when maxmemory is reached)
| Policy | Behavior |
|---|---|
noeviction | Returns errors on writes when memory full (default) |
allkeys-lru | Evicts least-recently-used key across all keys |
volatile-lru | Evicts LRU among keys with a TTL set |
allkeys-lfu | Evicts least-frequently-used key (better for skewed access patterns) |
volatile-lfu | LFU among keys with TTL |
allkeys-random | Random eviction |
volatile-random | Random eviction among keys with TTL |
volatile-ttl | Evicts keys with shortest remaining TTL first |
Best practice: If Redis is used purely as a cache, use allkeys-lru or allkeys-lfu. If Redis mixes cache data and durable data (e.g., session + persistent counters) in the same instance, use volatile-lru/volatile-lfu and make sure durable keys never get a TTL — but strongly consider separating cache and primary-store workloads into different instances instead.
4. Persistence: RDB & AOF
RDB (Redis Database Snapshot)
Point-in-time binary snapshot, triggered by SAVE (blocking) or BGSAVE (forks a child process, non-blocking).
save 900 1 # snapshot if >=1 write in 900s
save 300 10
save 60 10000
- Pros: compact single file, fast restarts, good for backups.
- Cons: data loss window between snapshots;
fork()on very large datasets can cause latency spikes (copy-on-write memory pressure).
AOF (Append Only File)
Logs every write operation; replayed on restart.
appendonly yes
appendfsync everysec # fsync every second (best balance)
# appendfsync always # fsync every write — safest, slowest
# appendfsync no # let OS decide — fastest, least safe
Since Redis 7, AOF uses a multi-part format (base file + incremental files + manifest), making rewrites safer and avoiding the old single-giant-file rewrite risk.
- Pros: much smaller data-loss window (≤1s with
everysec), human-auditable log. - Cons: larger file size than RDB, potentially slower restarts (replaying the whole log), though
AOF rewritecompacts it periodically.
Recommended production setup
Use both RDB + AOF together: RDB for fast, compact backups/restores; AOF for minimal data loss. Redis will use AOF on restart if enabled (more complete), falling back to RDB otherwise.
appendonly yes
appendfsync everysec
save 3600 1
save 300 100
save 60 10000
Best practice: Always test your restore procedure — a backup you’ve never restored is not a backup.
5. Replication
Redis replication is asynchronous, leader-follower (primary-replica). A replica connects, performs an initial full sync (RDB transfer), then receives a continuous command stream.
# On replica:
replicaof 10.0.0.1 6379
# or dynamically:
REPLICAOF 10.0.0.1 6379
REPLICAOF NO ONE # promote to primary
Key facts:
- Replication is asynchronous by default — a write can be acknowledged to the client before all replicas have it. Use
WAIT numreplicas timeoutif you need stronger durability guarantees before considering a write “safe.” - Replicas are read-only by default (
replica-read-only yes) — good practice, don’t disable this casually. - Chained replication (replica-of-replica) is supported to reduce load on the primary during fan-out.
Best practice: Never treat asynchronous replication as a substitute for durability (AOF/RDB) — it protects against node failure, not against data loss on a crash before replication completes.
6. Redis Sentinel (High Availability)
Sentinel provides automatic failover, monitoring, and service discovery for a primary-replica setup without sharding.
sentinel monitor mymaster 10.0.0.1 6379 2 # quorum = 2 sentinels must agree
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
- Run at least 3 Sentinel processes on separate hosts for quorum-based decisions (avoids split-brain from a single Sentinel’s false positive).
- Applications should connect via a Sentinel-aware client that discovers the current primary dynamically, not a hardcoded IP.
- Sentinel handles failover orchestration; it does not shard data — for that, use Redis Cluster.
7. Redis Cluster (Sharding)
Redis Cluster partitions data across multiple primary nodes using 16384 hash slots. Each key is mapped to a slot via CRC16(key) mod 16384.
redis-cli --cluster create \
10.0.0.1:6379 10.0.0.2:6379 10.0.0.3:6379 \
10.0.0.4:6379 10.0.0.5:6379 10.0.0.6:6379 \
--cluster-replicas 1
Hash Tags for Multi-Key Operations
Multi-key commands (MGET, transactions, Lua scripts) require all keys to live on the same slot. Force co-location using {} hash tags:
user:{1000}:profile
user:{1000}:settings
user:{1000}:sessions
All three hash to the same slot because only the substring inside {} is hashed.
- Cluster tolerates node failure via automatic failover (each primary should have ≥1 replica).
- Client must support cluster redirection (
MOVED/ASKresponses) — use a cluster-aware client library, never a plain single-node client. - Cross-slot multi-key operations will error (
CROSSSLOT) unless hash-tagged.
Best practice: Design your key schema with hash tags from day one if you anticipate needing Cluster — retrofitting hash tags into an existing large dataset is painful.
8. Transactions & Scripting
MULTI/EXEC
Queues commands and executes them atomically as a batch (not truly “rollback-on-error” like SQL transactions — a runtime error in one command doesn’t abort the others).
MULTI
INCR counter
LPUSH log "event"
EXEC
WATCH (Optimistic Locking)
Used for compare-and-swap style logic (read, compute, conditional write).
WATCH balance:user:1000
val = GET balance:user:1000
# ... compute new value in application ...
MULTI
SET balance:user:1000 newVal
EXEC # returns nil if balance:user:1000 changed since WATCH — retry logic needed
Lua Scripting (EVAL) and Functions
Scripts execute atomically (the whole script runs as one indivisible unit, blocking other commands).
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue
Since Redis 7, Redis Functions (FUNCTION LOAD) are the recommended replacement for ad-hoc EVAL scripts — they’re versioned, named, and persisted properly across replication/AOF, unlike scripts cached only in the script cache.
Best practice: Keep Lua scripts short. A slow script blocks the entire server just like a slow command — there is no timeout enforcement that safely interrupts a running script without an admin SCRIPT KILL intervention (which itself can’t interrupt write scripts safely).
9. Pub/Sub & Streams
Pub/Sub
Fire-and-forget messaging. No persistence, no delivery guarantee — if no subscriber is listening, the message is lost.
SUBSCRIBE news:tech
PUBLISH news:tech "Redis 8 released"
PSUBSCRIBE news:* # pattern subscribe
Use for: real-time notifications, cache-invalidation broadcast, live dashboards — never for anything requiring durability.
Streams (the durable alternative)
Streams solve Pub/Sub’s durability gap with an append-only log, consumer groups, and acknowledgment.
XADD orders:stream '*' order_id 123 status "paid"
# Consumer group setup
XGROUP CREATE orders:stream processors '$' MKSTREAM
XREADGROUP GROUP processors worker-1 COUNT 10 STREAMS orders:stream '>'
XACK orders:stream processors 1234567-0
XPENDING orders:stream processors # inspect unacknowledged messages
XCLAIM orders:stream processors worker-2 60000 1234567-0 # reassign stuck message
Best practice: Cap stream size to prevent unbounded growth:
XADD orders:stream MAXLEN ~ 100000 '*' order_id 123
The ~ makes trimming approximate (more efficient, avoids trimming exactly on every insert).
10. Common Design Patterns
Distributed Lock (Redlock-style, simplified single-instance)
SET lock:resource:42 "unique-random-token" NX EX 10
# ... critical section ...
# Release only if you still own it (avoid deleting someone else's lock):
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:resource:42 "unique-random-token"
For multi-instance safety, the Redlock algorithm acquires the lock across a majority of independent Redis nodes. Note: Redlock’s correctness under network partitions has been debated (see Martin Kleppmann’s critique) — for hard correctness guarantees, prefer a consensus-based lock service (e.g., Zookeeper, etcd) over Redis for safety-critical locking.
Rate Limiting (Sliding Window with Sorted Sets)
ZADD ratelimit:user:1000 <timestamp> <unique-request-id>
ZREMRANGEBYSCORE ratelimit:user:1000 0 <timestamp - window>
ZCARD ratelimit:user:1000 # if > limit, reject
EXPIRE ratelimit:user:1000 60
Rate Limiting (Simple Fixed Window)
INCR ratelimit:user:1000:minute:202608171530
EXPIRE ratelimit:user:1000:minute:202608171530 60
Leaderboard
ZADD leaderboard:game1 <score> <player_id>
ZREVRANGE leaderboard:game1 0 9 WITHSCORES
ZREVRANK leaderboard:game1 <player_id>
Job Queue (simple)
LPUSH queue:emails '{"to":"x@y.com","template":"welcome"}'
BRPOP queue:emails 0 # worker blocks until job arrives
For reliability (avoid losing a job if the worker crashes mid-processing), use BRPOPLPUSH/LMOVE into a “processing” list, then remove on success:
BLMOVE queue:emails queue:emails:processing 0 LEFT RIGHT
# on success:
LREM queue:emails:processing 1 <job>
For production-grade queues with retries, delayed jobs, and dead-letter handling, prefer Streams with consumer groups over raw lists.
Session Store
HSET session:abc123 user_id 1000 role "admin" ip "1.2.3.4"
EXPIRE session:abc123 1800
Caching (Cache-Aside)
1. App reads cache:key
2. If miss → read from DB → SET cache:key value EX ttl
3. If hit → return cached value
Idempotency Keys
SET idempotency:req:abc123 "processed" NX EX 86400
# if SET returns nil, request was already processed — skip reprocessing
Counters with Expiring Windows (Analytics)
INCR pageviews:2026-08-17
EXPIRE pageviews:2026-08-17 2592000 # keep 30 days
11. Caching Strategies
| Strategy | Description | Trade-off |
|---|---|---|
| Cache-Aside (Lazy Loading) | App checks cache, falls back to DB on miss, populates cache | Simple, but first request after eviction is slow; risk of thundering herd |
| Write-Through | Every write goes to cache and DB synchronously | Cache always fresh, but write latency increases |
| Write-Behind (Write-Back) | Write to cache immediately, async flush to DB | Fast writes, risk of data loss if cache crashes before flush |
| Read-Through | Cache layer itself fetches from DB on miss (via a caching library) | Cleaner app code, needs caching middleware |
Preventing Thundering Herd / Cache Stampede
When a hot key expires, many concurrent requests may simultaneously miss and hammer the DB. Mitigations:
- Probabilistic early expiration: recompute slightly before actual TTL, weighted by random probability.
- Locking: first request to miss acquires a short lock and recomputes; others wait or serve stale.
- Never-expire + background refresh: keep serving stale data while a background job refreshes it.
Preventing Cache Penetration (queries for non-existent keys)
Cache negative results too (short TTL), or use a Bloom filter to reject queries for keys that definitely don’t exist before hitting the DB.
Cache Key Design
- Use consistent, hierarchical naming:
<namespace>:<entity>:<id>:<field>e.g.app:user:1000:profile. - Include a version prefix (
v2:user:1000) to allow instant cache invalidation on schema changes by bumping the version.
12. Performance & Memory Optimization
Avoid Slow Commands in Production
KEYS *— scans the entire keyspace, blocking. UseSCAN(cursor-based, non-blocking) instead.
SCAN 0 MATCH user:* COUNT 100
FLUSHALL/FLUSHDB— obviously destructive, but also blocking on large datasets unlessASYNCis used:
FLUSHALL ASYNC
- Unbounded
SORT,SMEMBERS,HGETALL,LRANGE 0 -1on huge collections — useSSCAN/HSCAN/LRANGEwith bounded ranges/pagination instead.
Pipelining
Batch multiple commands in one round trip to eliminate network latency overhead:
redis-cli --pipe <<EOF
SET a 1
SET b 2
SET c 3
EOF
In client code, pipelining can turn thousands of round trips into one, often a 10-100x throughput improvement for bulk operations.
Memory Optimization
- Use
MEMORY USAGE <key>to inspect per-key memory cost. - Prefer hashes over many string keys for small objects (internal
listpackencoding is compact below configurable thresholds):
hash-max-listpack-entries 128
hash-max-listpack-value 64
- Same principle applies to small lists, sets, and sorted sets — tune
list-max-listpack-size,set-max-listpack-entries,zset-max-listpack-entries. - Use short key names and short field names at scale — every byte is duplicated across millions of keys.
- Consider
OBJECT ENCODING <key>to verify Redis is using the compact encoding you expect.
Connection & Command Efficiency
- Use
MGET/MSETinstead of looping singleGET/SET. - Use
EXPIREsparingly on hot loops; prefer setting TTL once at creation. - Avoid
DEBUGcommands andMONITORin production —MONITORin particular has severe throughput impact since it streams every command to the client.
13. Security Best Practices
- Never expose Redis directly to the internet. Bind to private interfaces only:
bind 127.0.0.1 10.0.0.5
protected-mode yes
- Require authentication:
requirepass "a-very-long-random-secret"
- Use ACLs (Redis 6+) instead of a single shared password — scope users to specific commands and key patterns:
ACL SETUSER app-readonly on >password ~app:* +get +mget -@all
ACL SETUSER app-writer on >password ~app:* +@read +@write -flushall -flushdb -config
- Rename or disable dangerous commands in production:
rename-command FLUSHALL ""
rename-command CONFIG "CONFIG_9f8a7b6c"
- Enable TLS for data in transit (Redis 6+ supports native TLS):
tls-port 6379
tls-cert-file /path/redis.crt
tls-key-file /path/redis.key
tls-ca-cert-file /path/ca.crt
- Network isolation: place Redis in a private subnet/VPC, restrict access via security groups/firewalls, never rely on
requirepassalone as your only defense. - Avoid storing plaintext secrets as values without application-level encryption if compliance requires it.
14. Monitoring & Observability
Key Metrics to Track
INFO all
Watch for:
used_memory/used_memory_rss/mem_fragmentation_ratio(ideal ~1.0–1.5; much higher indicates fragmentation)connected_clientsinstantaneous_ops_per_seckeyspace_hits/keyspace_misses(hit ratio = hits / (hits+misses))evicted_keys— non-zero and rising means you’re under memory pressureexpired_keysrejected_connectionsblocked_clientsmaster_repl_offset/ replication lag (INFO replicationon replicas)latest_fork_usec— high values indicateBGSAVE/BGREWRITEAOFfork latency impact
Slow Query Log
CONFIG SET slowlog-log-slower-than 10000 # log commands slower than 10ms (microseconds unit)
SLOWLOG GET 25
SLOWLOG RESET
Latency Monitoring
CONFIG SET latency-monitor-threshold 100 # ms
LATENCY HISTORY command
LATENCY LATEST
Recommended Tooling
redis-cli --latency/--latency-historyfor live latency sampling.redis-cli --bigkeysto scan for oversized keys that risk blocking operations or memory imbalance.- Prometheus +
redis_exporterfor time-series metrics and alerting. redis-cli --statfor a live-updating summary view.
15. Anti-Patterns to Avoid
- Using
KEYS *in application code — alwaysSCAN. - Storing huge values in a single key (multi-MB blobs) — degrades performance for that shard/slot and complicates replication; consider chunking or external object storage with Redis only holding a pointer.
- No TTL on cache keys — leads to unbounded memory growth and stale data.
- One giant Redis instance for everything (cache + queue + session + pub/sub) — a slow command or eviction storm in one workload disrupts unrelated workloads; isolate by use case where practical.
- Ignoring
maxmemory-policy— leaving it asnoevictionon a cache-only instance causes write errors instead of graceful eviction. - Treating replication as backup — it isn’t; a bad
DEL/FLUSHALLreplicates instantly to all replicas. - Not hash-tagging keys that need multi-key atomic operations before scaling to Cluster.
- Blindly trusting client-side connection pools without health checks — stale connections after failover cause silent errors.
- Running
MONITORin production for debugging — significant throughput hit. - Not setting
appendfsync everysecor equivalent durability for data you can’t afford to lose, while assuming “Redis persists to disk” is automatically safe. - Ignoring
mem_fragmentation_ratio— over time fragmentation can silently consume far more RSS thanused_memorysuggests; consideractivedefrag yesfor long-lived instances. - Synchronous cross-region calls to Redis — always deploy Redis close (same AZ/region) to its application tier; cross-region round trips defeat the purpose of an in-memory store.
16. Client Libraries & Connection Pooling
- Always use a connection pool, never open a new TCP connection per request.
- Popular clients:
redis-py(Python),ioredis/node-redis(Node.js),Jedis/Lettuce(Java),go-redis(Go),StackExchange.Redis(.NET). - For Cluster deployments, use the cluster-aware variant of your client (e.g.,
redis-py-clustersupport built intoredis-py>=4,ioredisCluster mode,Lettucecluster client) — these correctly handleMOVED/ASKredirections and slot caching. - Configure sane timeouts (
socket_timeout,connect_timeout) — a hung Redis call without a timeout can cascade into application-wide thread pool exhaustion. - Prefer clients that support RESP3 protocol (Redis 6+) for features like client-side caching and better push-message support.
17. Backup, Disaster Recovery & Upgrades
- Schedule regular
BGSAVEsnapshots and ship RDB files off-host (S3, GCS, etc.) on a rotation. - Test restore procedures on a non-production instance regularly — an untested backup is a liability.
- For major version upgrades, always test against a replica first, review the release’s breaking-changes notes, and roll out with a plan to fail back to the previous version.
- For Cluster upgrades, use rolling replica-first upgrades: upgrade replicas, fail over, then upgrade the former primaries.
- Keep
redis.confunder version control and treat configuration as code — untracked manualCONFIG SETchanges are a common source of “it worked before the restart” incidents (sinceCONFIG SETis runtime-only unless followed byCONFIG REWRITE).
Quick Reference: Command Complexity Cheat Sheet
| Command | Complexity | Note |
|---|---|---|
GET/SET | O(1) | |
HGET/HSET | O(1) | |
LPUSH/RPUSH | O(1) | |
LRANGE | O(S+N) | S=start offset, N=elements returned — avoid large N |
SADD/SISMEMBER | O(1) avg | |
ZADD | O(log N) | |
ZRANGE | O(log N + M) | M=elements returned |
KEYS | O(N) | Avoid in production |
SCAN | O(1) per call | Cursor-based, safe for production |
SORT | O(N log N) | Can be expensive on large collections |
FLUSHALL | O(N) | Use ASYNC |
This guide reflects Redis behavior as of the Redis 7.x/8.x generation. Always cross-check against the official documentation at redis.io for the exact version you’re running, since defaults and available commands evolve between releases.