Getting Started
Connect to Redis
redis-cli is the standard command-line client. PING tests connectivity (returns PONG). Always use the AUTH command or -a flag in production. Avoid putting passwords on the command line in shared environments — use the REDISCLI_AUTH environment variable instead. Modern Redis (6+) supports ACL-based users with scoped permissions.
# connect to a local server (default port 6379)
redis-cli
# connect with a specific host and port
redis-cli -h host.example.com -p 6380
# connect with authentication
redis-cli -a "yourpassword"
redis-cli -h host -p 6379 -a "yourpassword"
# connect over TLS
redis-cli --tls --cert client.crt --key client.key -h host -p 6379
# test the connection
PING # returns PONG
PING "hello" # returns "hello"Database Selection & Basics
Redis databases are logical namespaces (numbered 0-15), not isolated schemas — they share the same memory. Most applications use only db 0. FLUSHDB clears the current db; FLUSHALL clears everything. Multiple databases are discouraged in favor of separate Redis instances or key prefixes, since they share a single thread and can't be individually configured.
# Redis has 16 logical databases (0-15) by default
SELECT 0 # switch to database 0
SELECT 15 # switch to database 15
# move a key to another database
MOVE mykey 1
# flush the current database (DESTRUCTIVE)
FLUSHDB
# flush ALL databases (VERY DESTRUCTIVE)
FLUSHALL
# get the number of keys in the current db
DBSIZE
# get basic server info
INFO server
INFO memory
INFO replicationKeys & General Operations
KEYS blocks the server while scanning — never use it in production on large datasets; use SCAN instead (cursor-based, non-blocking). Keys should be structured with colons (user:1:profile) for readability. EX sets expiration in seconds; PX in milliseconds. TYPE returns the data structure (string, list, hash, set, zset, stream).
# set and get a key
SET user:1 "Alice"
GET user:1
# check if a key exists
EXISTS user:1 # 1 if exists, 0 if not
# delete keys
DEL user:1
DEL key1 key2 key3 # returns number deleted
# set a key with expiration (seconds)
SET session:abc "data" EX 3600
# find keys matching a pattern (avoid in production)
KEYS user:*
SCAN 0 MATCH user:* COUNT 100 # safer iteration
# rename a key
RENAME oldkey newkey
# get the type of a value stored at a key
TYPE user:1Expiration & TTL
TTL returns -2 for non-existent keys and -1 for keys without an expiration. Expiration is lazy — Redis only checks when the key is accessed or during periodic background sweeps. SET with EX is atomic (no race between SET and EXPIRE). Expired keys don't trigger events by default; enable keyspace notifications if you need them.
# set expiration on an existing key
EXPIRE user:1 60 # 60 seconds
EXPIREAT user:1 1700000000 # Unix timestamp
# set expiration in milliseconds
PEXPIRE user:1 60000
# view remaining time to live (seconds)
TTL user:1 # -2 if no key, -1 if no expiry
# view TTL in milliseconds
PTTL user:1
# remove expiration (make key persistent)
PERSIST user:1
# set value AND expiration atomically
SET token "abc" EX 3600SCAN & Safe Iteration
SCAN is the production-safe alternative to KEYS — it returns a cursor you follow until 0. COUNT is a hint, not exact (it may return more or fewer). SCAN offers weak guarantees: it may return duplicates or miss keys added during iteration, but it never blocks. Use HSCAN/SSCAN/ZSCAN for collection types. Always handle the cursor loop in your code.
# SCAN: cursor-based iteration (non-blocking)
SCAN 0 MATCH user:* COUNT 100
# returns: [nextCursor, [key1, key2, ...]]
SCAN <nextCursor> MATCH user:* COUNT 100
# continue until cursor returns to 0
# scan a specific type
SCAN 0 TYPE hash
# HSCAN: iterate hash fields
HSCAN myhash 0 MATCH field* COUNT 100
# SSCAN: iterate set members
SSCAN myset 0 MATCH member* COUNT 100
# ZSCAN: iterate sorted set members
ZSCAN myzset 0 MATCH member* COUNT 100Data Types Overview
Core Data Structures
Choosing the right data structure is the key Redis skill. Strings for counters/caches, hashes for objects, lists for queues, sets for uniqueness/tags, sorted sets for leaderboards/rankings, streams for event logs. Each operation on a data structure is atomic, so concurrent clients can't corrupt state. Pick the structure that matches your access pattern.
# Redis is a data structure server, not just a key-value store
# Each key holds a typed data structure:
# String: binary-safe blob (up to 512MB)
SET key "value"
SET counter 100
# List: ordered, linked-list of strings
LPUSH mylist "a" "b"
RPUSH mylist "c"
# Hash: field-value map (like a small object)
HSET user:1 name "Alice" age 30
# Set: unordered collection of unique strings
SADD tags "redis" "db"
# Sorted Set (ZSet): set scored by a float
ZADD leaderboard 100 "alice" 200 "bob"
# Stream: append-only log with IDs
XADD mystream * field value
# Bitmap, HyperLogLog, Geo: built on strings/zsetsChoosing the Right Type
Pick the type by access pattern: need uniqueness? Set. Need ordering by score? Sorted set. Need to update one field of an object? Hash (not a serialized string). Need approximate unique counts at scale? HyperLogLog (12KB for billions of items). Right type choice = simpler code, atomic ops, and dramatic memory savings.
# Use case -> data type mapping:
# Cache a string -> String
SET api:response "json_blob"
# Store an object -> Hash (one key, multiple fields)
HSET user:1 name "Alice" email "[email protected]" age 30
# Queue / stack -> List
LPUSH tasks "job1" # push to head
RPOP tasks # pop from tail (FIFO queue)
# Unique tags -> Set
SADD post:1:tags "redis" "db" "cache"
# Leaderboard -> Sorted Set
ZADD scores 100 "alice" 250 "bob"
ZREVRANGE scores 0 9 # top 10
# Event log -> Stream
XADD events * type click user 1
# Count unique -> HyperLogLog (approximate, fixed memory)
PFADD visitors "user1" "user2"Memory Encoding
Redis uses compact encodings (listpack, intset) for small structures — saving 5-10x memory — and switches to hash tables/skiplists automatically as they grow. OBJECT ENCODING reveals the current encoding. MEMORY USAGE reports bytes. You can tune the threshold (e.g., hash-max-listpack-entries) but the defaults are well-chosen. Understanding encodings helps with capacity planning.
# Redis optimizes storage based on size
# Inspect the internal encoding of a key
OBJECT ENCODING mykey
# String encodings:
# "embstr" - short strings (< 44 bytes)
# "raw" - long strings
# "int" - integer values
# Hash/List/Set/ZSet encodings:
# "listpack" / "intset" - small (compact, memory-efficient)
# "hashtable" / "list" / "skiplist" - large (faster, more memory)
# check memory usage of a key
MEMORY USAGE mykey
# sample memory usage stats
MEMORY STATS
# the encoding changes automatically as the structure growsOBJECT & Memory Commands
OBJECT IDLETIME is useful for finding cold cache keys (rarely accessed). OBJECT FREQ works only with maxmemory-policy LFU. MEMORY USAGE with SAMPLES 0 counts exactly (default samples for large collections). COMMAND DOCS (Redis 7+) returns structured documentation. These introspection tools help debug memory bloat and tune eviction policies.
# OBJECT subcommands for introspection
OBJECT ENCODING mykey # internal encoding
OBJECT REFCOUNT mykey # reference count
OBJECT IDLETIME mykey # seconds since last access
OBJECT FREQ mykey # access frequency (LFU mode)
# check memory usage (bytes) of a key
MEMORY USAGE mykey
MEMORY USAGE mykey SAMPLES 0 # exact count
# total memory used by the server
INFO memory | grep used_memory_human
# help understand a command
COMMAND INFO GET
COMMAND DOCS SET
# COMMAND lists all commands (huge output)
COMMANDType Errors & Type Checking
A key's type is fixed for its lifetime — you can't reuse the same key for a different type without deleting it first. WRONGTYPE errors protect against accidental type confusion. Always check TYPE when working with keys of unknown origin. This strict typing is why Redis commands are type-prefixed (HSET vs SET vs SADD).
# each key has ONE type; using the wrong command fails
SET mykey "hello"
LPUSH mykey "x" # WRONGTYPE error
# WRONGTYPE Operation against a key holding the wrong kind of value
# check the type before operating
TYPE mykey # "string"
if [ "$(TYPE mykey)" = "string" ]; then ...
# TYPE returns: string|list|hash|set|zset|stream|none
# rename preserves the type
RENAME mykey newkey
TYPE newkey # still "string"
# type is immutable; delete + recreate to change types
DEL mykey
LPUSH mykey "x" # now a listStrings
Basic String Operations
Strings are binary-safe (can hold any bytes up to 512MB, including images or JSON). SET with NX is the classic Redis lock primitive (set if not exists). GETSET is atomic and useful for swapping configs. SETRANGE can grow a string. For structured data, prefer Hashes over serialized JSON strings — they allow field-level updates.
# set and get
SET mykey "hello"
GET mykey # "hello"
# set only if not exists (NX) or only if exists (XX)
SET mykey "new" NX # set only if key does NOT exist
SET mykey "new" XX # set only if key DOES exist
SET mykey "new" EX 60 NX # with expiry, only if new
# get and set atomically (returns old value)
GETSET counter 0 # returns old value, sets new
# append to a string
APPEND mykey " world" # "hello world"
# get substring
GETRANGE mykey 0 4 # "hello"
SETRANGE mykey 6 "redis"# "hello redis"
# get string length
STRLEN mykey # 11Numeric Counters
INCR/DECR are atomic — safe for concurrent access without locks. This makes Redis ideal for counters, rate limiting (INCR + EXPIRE), and generating sequence IDs. INCRBYFLOAT supports decimals but uses double precision (watch for rounding). If the value isn't an integer, INCR returns an error. Counters work best when combined with EXPIRE for time-bucketed stats.
# increment a numeric string atomically
INCR counter # counter = counter + 1
INCRBY counter 10 # counter = counter + 10
# decrement
DECR counter # counter = counter - 1
DECRBY counter 5 # counter = counter - 5
# floating point operations
SET price "10.50"
INCRBYFLOAT price 0.25 # "10.75"
INCRBYFLOAT price -1.00 # "9.75"
# all increment ops are atomic and concurrency-safe
# use for: counters, rate limiting, sequence IDs, statsMultiple Key Operations
MSET/MGET batch operations into one round-trip — essential for performance (Redis single-threaded model makes per-command overhead matter). MSETNX is atomic: either all keys are set or none (all-or-nothing). GETDEL (6.2+) atomically returns and deletes — useful for one-time-read message patterns. Batch commands whenever possible to reduce network latency.
# set multiple keys at once (atomic)
MSET k1 "v1" k2 "v2" k3 "v3"
# get multiple keys at once
MGET k1 k2 k3 # ["v1", "v2", "v3"]
# set multiple only if ALL keys are new
MSETNX k1 "v1" k4 "v4" # 1 if all set, 0 if any existed
# MSET/MGET reduce round-trips — much faster than
# separate SET/GET calls in a loop
# get the length of a string value
STRLEN k1 # 2
# atomic get-and-delete (useful for queues)
GETDEL mykey # returns value AND deletes keyBitmaps (String-based)
Bitmaps store boolean flags for up to 4 billion users in ~512MB — extremely space-efficient for large populations. BITOP AND/OR/XOR/NOT combine bitmaps (e.g., users active on multiple days). BITCOUNT counts active users. Use cases: daily active users, feature flags, attendance tracking. Position = user ID.
# bitmaps are strings operated on at the bit level
# great for boolean flags on a large user base
# set bit at position (0 or 1)
SETBIT user:1:active 7 1 # user 1 active
GETBIT user:1:active 7 # 1
# count set bits
BITCOUNT user:1:active # total bits set to 1
# bitwise operations between strings
SETBIT users:daily:2025-01-01 5 1
SETBIT users:daily:2025-01-02 5 1
BITOP AND active-both users:daily:2025-01-01 users:daily:2025-01-02
BITCOUNT active-both # users active on both days
# find the first set bit
BITPOS user:1:active 1 # position of first 1-bitBitfield Operations
BITFIELD packs multiple small counters into one string — e.g., 1000 daily counters as u8 in 1KB. OVERFLOW SAT saturates at max (good for stats), FAIL returns nil on overflow (good for caps), WRAP wraps around (default, usually unwanted). This is far more memory-efficient than separate keys for many small counters.
# BITFIELD: multiple counters in one string (compact)
# store up to 2^63 counters, each 1-64 bits, in a single key
# set a 8-bit unsigned counter at offset 0
BITFIELD mycount SET u8 0 100
# increment a counter (with overflow control)
BITFIELD mycount INCRBY u8 0 10 # returns 110
BITFIELD mycount INCRBY u8 0 10 # returns 120
# overflow control: WRAP (default), SAT, FAIL
BITFIELD mycount OVERFLOW SAT INCRBY u8 0 200 # saturates at 255
BITFIELD mycount OVERFLOW FAIL INCRBY u8 0 999 # returns nil (overflow)
# read multiple counters in one call
BITFIELD mycount GET u8 0 GET u8 8 GET u8 16Hashes
Hash Basics
Hashes are ideal for storing objects — one key per object, fields per property. This beats serializing JSON because you can update individual fields atomically (HSET) without read-modify-write. HGETALL returns all fields; for large hashes, prefer HSCAN. Hashes use listpack encoding when small (under ~128 fields), making them memory-efficient.
# a hash maps fields to values (like a small object)
HSET user:1 name "Alice" age 30 email "[email protected]"
# get a single field
HGET user:1 name # "Alice"
# get multiple fields
HMGET user:1 name age # ["Alice", "30"]
# get all fields and values
HGETALL user:1
# name -> Alice
# age -> 30
# email -> [email protected]
# get all field names or values
HKEYS user:1 # [name, age, email]
HVALS user:1 # [Alice, 30, [email protected]]
# get the number of fields
HLEN user:1 # 3Hash Field Operations
HSETNX provides field-level 'set if not exists' (useful for one-time init). HINCRBY on hash fields is atomic — perfect for per-user counters (login count, view count). Hashes auto-delete when the last field is removed (HDEL). HMSET is deprecated since HSET now accepts multiple fields; always use HSET.
# set a field only if it doesn't exist
HSETNX user:1 status "new" # 1 if set, 0 if existed
# delete a field
HDEL user:1 email # removes the email field
# check if a field exists
HEXISTS user:1 name # 1 if exists, 0 if not
# increment a numeric field atomically
HINCRBY user:1 age 1 # age = 31
HINCRBY user:1 age -5 # age = 26
HINCRBYFLOAT user:1 score 0.5
# get the string length of a field's value
HSTRLEN user:1 name # 5 (length of "Alice")
# set multiple fields (same as HSET with multiple)
HMSET user:1 a 1 b 2 # deprecated, use HSETHash Iteration
HGETALL blocks the server for large hashes — use HSCAN for production iteration. HSCAN is cursor-based: call repeatedly until the cursor returns to 0. COUNT is a hint. HRANDFIELD (6.2+) is useful for sampling (e.g., random feature flags). For hashes with millions of fields, consider breaking them into multiple keys by sharding.
# HSCAN: cursor-based iteration (for large hashes)
HSCAN user:1 0 MATCH "na*" COUNT 10
# returns [cursor, [field, value, field, value, ...]]
# iterate all fields
cursor=0
while true:
cursor, fields = HSCAN user:1 cursor COUNT 100
process(fields)
if cursor == 0: break
# HGETALL is fine for small hashes (< 100 fields)
# HSCAN is required for large hashes to avoid blocking
# get a random field
HRANDFIELD user:1 # one random field name
HRANDFIELD user:1 3 # 3 random field names
HRANDFIELD user:1 3 WITHVALUES # fields + valuesHash Expiration (Redis 7.4+)
Field-level TTL (7.4+) is a major feature for hashes — previously you needed separate keys or workarounds to expire individual fields. Useful for objects with mixed lifetimes (a user profile where the session token expires but the name persists). Older Redis versions require storing expiring fields as separate keys with their own EXPIRE.
# before 7.4, expiration was only at the key level
EXPIRE user:1 3600 # the whole hash expires
# Redis 7.4+: field-level TTL (HEXPIRE)
HEXPIRE user:1 60 FIELDS 1 session_token
# expires only the session_token field in 60 seconds
# get TTL of a specific field
HPTTL user:1 FIELDS 1 session_token
# persist a specific field (remove its TTL)
HPERSIST user:1 FIELDS 1 session_token
# get all fields and their expiration times
HEXPIRETIME user:1 FIELDS 1 session_tokenHash Use Cases
Hashes shine for objects and grouped counters. A powerful memory technique: when you have many small string keys, group them into one hash — the listpack encoding uses far less overhead than thousands of individual keys (each with metadata). This 'key bucketing' can cut memory usage by 5-10x for small values.
# 1. Store an object (user profile, config)
HSET config:app name "MyApp" version "2.0" debug "off"
# 2. Per-user counters
HINCRBY user:1:stats logins 1
HINCRBY user:1:stats page_views 1
HGET user:1:stats logins
# 3. Shopping cart (product_id -> quantity)
HSET cart:user:1 product:100 2
HINCRBY cart:user:1 product:100 1
HDEL cart:user:1 product:100
# 4. Feature flags
HSET feature_flags user:1 new_ui 1 beta 0
HGET feature_flags user:1 new_ui # 1
# 5. Group small key-value pairs into one hash (memory savings)
# instead of SET k1 v1; SET k2 v2 -> HSET bucket k1 v1 k2 v2Lists
List Basics (Push/Pop)
Lists are double-ended, so LPUSH+RPOP = queue (FIFO), LPUSH+LPOP = stack (LIFO). BLPOP/BRPOP block until an item is available — the foundation of Redis queues (no polling needed). Always set a timeout on BLPOP to handle disconnects. Lists are O(1) for head/tail operations but O(N) for middle access.
# lists are ordered sequences of strings (linked lists)
# push to head (left) or tail (right)
LPUSH mylist "a" "b" # list: [b, a]
RPUSH mylist "c" # list: [b, a, c]
# pop from head or tail
LPOP mylist # "b" (removes from head)
RPOP mylist # "c" (removes from tail)
# pop multiple
LPOP mylist 2 # ["a", ...]
# get length
LLEN mylist # 0
# blocking pop (waits if empty, up to timeout)
BLPOP queue:tasks 30 # blocks up to 30 seconds
BRPOP queue:tasks 0 # blocks foreverList Indexing & Ranges
LRANGE 0 -1 returns the whole list. LTRIM is a common pattern to cap list size (e.g., keep only the latest 100 events). LREM removes by value (count: positive=head, negative=tail, 0=all). LINDEX is O(N) for middle elements — lists are optimized for end access. For random access by index, use a different structure.
# get element by index (0-based, O(N) traversal)
LINDEX mylist 0 # first element
LINDEX mylist -1 # last element
# get a range of elements
LRANGE mylist 0 -1 # all elements
LRANGE mylist 0 2 # first 3 elements
LRANGE mylist -3 -1 # last 3 elements
# set an element by index
LSET mylist 1 "new"
# get length
LLEN mylist
# trim to a range (removes everything outside)
LTRIM mylist 0 99 # keep only first 100 elements
# remove elements by value
LREM mylist 2 "value" # remove first 2 occurrences of "value"
LREM mylist -2 "value" # remove last 2 occurrencesList as a Queue
BRPOPLPUSH/BLMOVE moves an item atomically from source to destination — the consumer can process it safely, and if it crashes, the item is still in the destination (reliable queue pattern). This beats BRPOP+processing because a crash after BRPOP loses the item. After processing, delete from the destination list.
# simple FIFO queue: LPUSH to add, RPOP to consume
LPUSH tasks "job1" "job2"
RPOP tasks # "job1" (FIFO order)
# reliable queue with BRPOP (blocking, waits for work)
BRPOP tasks 0 # blocks until a task is available
# move items between lists atomically
LPUSH source "item"
RPOPLPUSH source destination # move one item
# blocking move (reliable queue pattern)
BRPOPLPUSH source destination 30 # blocks up to 30s
# Redis 6.2+: BLMOVE replaces BRPOPLPUSH
BLMOVE source destination RIGHT LEFT 30
# capped queue (e.g., recent events)
LPUSH recent "event"
LTRIM recent 0 99 # keep latest 100List Insertion & Removal
LINSERT is O(N) — use sparingly on large lists. LPOS finds element positions without removing them. LMPOP (7.0+) pops from multiple lists in one call, useful for prioritized queues. For frequent middle-of-list operations, a list is the wrong choice — consider a sorted set or a different data model.
# insert before or after a pivot element
LINSERT mylist BEFORE "pivot" "new"
LINSERT mylist AFTER "pivot" "new"
# remove elements by value
LREM mylist 1 "value" # remove first occurrence from head
LREM mylist -1 "value" # remove first occurrence from tail
LREM mylist 0 "value" # remove ALL occurrences
# pop and push in one atomic operation
RPOPLPUSH source dest
# get and remove from both ends (6.2+)
LPOP mylist 2 # pop 2 from head
LMPOP 2 mylist LEFT COUNT 3 # (7.0+) pop from one of multiple lists
# find the position of an element (6.0+)
LPOS mylist "value"
LPOS mylist "value" RANK 2 # find 2nd occurrenceList Use Cases
Lists are best for sequences with end-access patterns: queues, stacks, feeds, buffers. LTRIM makes them ideal for capped logs/feeds. For rate limiting, a list of timestamps + LTRIM is a simple sliding window. For sorted/prioritized queues, use a sorted set instead. Lists don't support deduplication — use a set for that.
# 1. Task queue (FIFO)
LPUSH tasks "process_order:123"
BRPOP tasks 30
# 2. Recent activity feed (capped)
LPUSH user:1:feed "post:99" "post:98"
LTRIM user:1:feed 0 49 # latest 50
# 3. Stack (LIFO)
LPUSH stack "item"
LPOP stack
# 4. Circular buffer (limited size)
LPUSH buffer "new"
LTRIM buffer 0 999 # keep 1000 items
# 5. Rate limiter (sliding window)
LPUSH rate:user:1 <timestamp>
LTRIM rate:user:1 0 99
LLEN rate:user:1 # request count in windowSets
Set Basics
Sets store unique values — perfect for tags, categories, and deduplication. SISMEMBER is O(1) — far faster than checking a list. SMEMBERS can block on large sets; use SSCAN for iteration. SRANDMEMBER doesn't remove; SPOP does. Sets use intset (sorted array) when all members are integers — very memory-efficient.
# sets: unordered collection of unique strings
SADD tags "redis" "db" "cache"
SADD tags "redis" # ignored (already exists)
# remove a member
SREM tags "cache"
# check membership
SISMEMBER tags "redis" # 1 if member, 0 if not
# get the number of members
SCARD tags # 2
# get all members (avoid on large sets)
SMEMBERS tags # ["redis", "db"]
# get a random member
SRANDMEMBER tags # one random member
SRANDMEMBER tags 3 # 3 random (may repeat)
SRANDMEMBER tags -3 # 3 unique random
# pop a random member (removes it)
SPOP tags # removes and returns oneSet Operations
Set operations are Redis's superpower — union, intersection, and difference in O(N) without client-side processing. Use cases: mutual friends (SINTER), unique visitors (SUNION), tag-based filtering (SINTER of tag sets). SINTERCARD (7.0+) returns just the count, faster when you don't need the members. Store results to avoid recomputation.
# set union, intersection, difference
SADD set1 "a" "b" "c"
SADD set2 "b" "c" "d"
# union (all unique members from all sets)
SUNION set1 set2 # a, b, c, d
# intersection (members in ALL sets)
SINTER set1 set2 # b, c
# difference (members in set1 but NOT in set2)
SDIFF set1 set2 # a
# store the result in a new set
SUNIONSTORE result set1 set2
SINTERSTORE result set1 set2
SDIFFSTORE result set1 set2
# count intersection without returning members (7.0+)
SINTERCARD 2 set1 set2 LIMIT 0Set Iteration & Move
SMOVE is atomic — useful for state transitions (e.g., move a user from 'pending' to 'active' set). SSCAN is the safe way to iterate large sets. SMISMEMBER (7.4+) batch-checks membership, reducing round-trips. BSPOP (7.0+) blocks like BLPOP but for sets — useful for event-driven patterns where order doesn't matter.
# SSCAN: cursor-based iteration for large sets
SSCAN myset 0 MATCH "pre*" COUNT 100
# returns [cursor, [member1, member2, ...]]
# move a member from one set to another atomically
SMOVE source dest "member"
# pop multiple random members (6.2+)
SPOP myset 3 # removes and returns 3 members
# blocking set pop (7.0+) - waits until a member exists
BSPOP myset 30 # blocks up to 30 seconds
# check multiple memberships at once (7.4+)
SMISMEMBER myset "a" "b" "c"
# returns [1, 0, 1]Set Use Cases
Sets are the go-to for uniqueness and relationships. The SINTER pattern for mutual followers/friends is a classic. For 'visitors per day' with huge cardinality, consider HyperLogLog (approximate but fixed 12KB). For ordered unique data (leaderboards), use a sorted set. Sets are unordered — if order matters, use a list or sorted set.
# 1. Tags / categories
SADD post:1:tags "redis" "db"
SADD post:2:tags "redis" "cache"
SINTER post:1:tags post:2:tags # posts sharing "redis"
# 2. Unique visitors per day
SADD visitors:2025-01-01 "user:1" "user:2"
SCARD visitors:2025-01-01 # visitor count
# 3. Followers / following
SADD user:1:following "user:2" "user:3"
SADD user:2:following "user:1" "user:3"
SINTER user:1:following user:2:following # mutual follows
# 4. Blacklist / whitelist
SADD blacklist "ip:1.2.3.4"
SISMEMBER blacklist "ip:1.2.3.4"
# 5. Lottery / random selection
SADD participants "u1" "u2" "u3" "u4"
SPOP participants 1 # draw a winnerSets vs Other Types
Type selection is the core Redis design decision. Set for uniqueness + fast membership (no order). Sorted set for rankings/ordering (costs more memory). List for sequences with duplicates. Hash for field-value objects. When unsure, sketch your access patterns: reads, writes, ordering, and uniqueness requirements — the right type becomes obvious.
# Set: unordered, unique, O(1) membership check
SADD myset "a" "b"
SISMEMBER myset "a" # O(1)
# Sorted Set: unique + ordered by score
ZADD myzset 1 "a" 2 "b"
ZRANK myzset "a" # 0 (rank)
# List: ordered, allows duplicates, O(N) search
LPUSH mylist "a" "a"
LPOS mylist "a" # 0 (first match)
# Hash: field-value pairs, no ordering
HSET myhash a 1 b 2
# Choose:
# - need uniqueness only? -> Set
# - need uniqueness + ordering? -> Sorted Set
# - need ordering + allow dups? -> List
# - need key-value mapping? -> HashSorted Sets
Sorted Set Basics
Sorted sets (zsets) are Redis's most powerful structure — unique members ordered by score, with O(log N) operations. ZADD options (NX/XX/GT/LT) enable conditional updates: GT only updates if the new score is greater (great for 'best score' tracking). Scores are doubles; members are unique. Ties are broken lexicographically.
# sorted set: unique members ordered by a float score
ZADD leaderboard 100 "alice" 200 "bob" 150 "carol"
# update a score (re-add with new score)
ZADD leaderboard 250 "alice" # alice's score becomes 250
# add with options (NX: only new, XX: only existing, GT/LT: conditional)
ZADD leaderboard NX 300 "new"
ZADD leaderboard GT 300 "alice" # only if new score is greater
# get a member's score
ZSCORE leaderboard "alice" # "250"
# get a member's rank (0-based, ascending)
ZRANK leaderboard "alice" # rank from lowest
ZREVRANK leaderboard "alice" # rank from highest (0 = top)
# get the number of members
ZCARD leaderboard # 3Range Queries
ZRANGE returns by rank (index); ZRANGEBYSCORE returns by score. The '(' prefix makes a score exclusive. Redis 6.2 unified these into ZRANGE with BYSCORE/BYLEX/REV options. WITHSCORES includes the score in output. For leaderboards, ZREVRANGE 0 9 gives the top 10. ZRANGESTORE (6.2+) stores the result in a new key.
# get members by index range (ascending)
ZRANGE leaderboard 0 -1 # all, ascending
ZRANGE leaderboard 0 2 # first 3 (lowest scores)
ZRANGE leaderboard -3 -1 # last 3 (highest scores)
# get members by score range
ZRANGEBYSCORE leaderboard 100 200 # scores 100-200
ZRANGEBYSCORE leaderboard 100 +inf # scores >= 100
ZRANGEBYSCORE leaderboard (100 200 # scores > 100 (exclusive)
# descending order
ZREVRANGE leaderboard 0 2 # top 3 (highest scores)
ZREVRANGEBYSCORE leaderboard 200 100 # 200 down to 100
# with scores
ZRANGE leaderboard 0 -1 WITHSCORES
# Redis 6.2+: unified ZRANGE with BYSCORE/REV options
ZRANGE leaderboard 100 200 BYSCORE
ZRANGE leaderboard 200 100 BYSCORE REVScore Operations & Ranks
ZINCRBY atomically adjusts a score and re-sorts — perfect for live leaderboards. ZREMRANGEBYRANK is great for keeping a zset bounded (e.g., keep only top 1000). ZCOUNT gives the count in a range without fetching members. ZMSCORE (6.2+) batch-fetches scores, reducing round-trips for multi-member lookups.
# increment a member's score
ZINCRBY leaderboard 50 "alice" # alice += 50
# get the score
ZSCORE leaderboard "alice"
# remove a member
ZREM leaderboard "bob"
# remove members by rank range
ZREMRANGEBYRANK leaderboard 0 9 # remove lowest 10
ZREMRANGEBYSCORE leaderboard 0 100 # remove scores <= 100
# count members in a score range
ZCOUNT leaderboard 100 200
# get the rank
ZRANK leaderboard "alice" # ascending rank
ZREVRANK leaderboard "alice" # descending rank (0 = highest)
# get multiple members' scores at once
ZMSCORE leaderboard "alice" "bob" # ["250", "200"]Lexicographical Ranges (BYLEX)
BYLEX enables range queries on members when scores are equal — turning a sorted set into an auto-sorted string index. Use cases: autocomplete (store words with score 0, query prefix ranges), sorted dictionaries. '[a' means inclusive, '(a' exclusive, '-' start, '+' end. This only works when all scores are identical.
# when all scores are equal, sorted sets act as a sorted string set
ZADD myset 0 "apple" 0 "banana" 0 "cherry" 0 "date"
# range by lexicographical order
ZRANGEBYLEX myset "[a" "[c" # apple, banana, cherry
ZRANGEBYLEX myset "(a" "[c" # banana, cherry (a exclusive)
ZRANGEBYLEX myset "-" "[b" # from start to banana
ZRANGEBYLEX myset "[c" "+" # cherry to end
# count in a lex range
ZLEXCOUNT myset "[a" "[c" # 3
# remove by lex range
ZREMRANGEBYLEX myset "[a" "[b"
# Redis 6.2+: use ZRANGE with BYLEX
ZRANGE myset "[a" "[c" BYLEXSorted Set Aggregations
ZUNIONSTORE/ZINTERSTORE combine zsets with configurable aggregation (SUM/MIN/MAX) and weights — powerful for weighted scoring (e.g., relevance = text_match*2 + recency*1). Redis 6.2 added ZUNION/ZINTER/ZDIFF that return results without storing (one less command). These are O(N*M) — be careful with many large sets. Set count must precede the set names.
# ZUNIONSTORE / ZINTERSTORE: combine multiple sorted sets
ZADD set1 1 "a" 2 "b"
ZADD set2 2 "a" 3 "c"
# union: sum scores by default (a = 1+2 = 3)
ZUNIONSTORE result 2 set1 set2
ZRANGE result 0 -1 WITHSCORES # a=3, b=2, c=3
# specify aggregation: SUM (default), MIN, MAX
ZUNIONSTORE result 2 set1 set2 AGGREGATE MAX # a=2, b=2, c=3
# apply weights to each set's scores
ZUNIONSTORE result 2 set1 set2 WEIGHTS 2 1 # set1 scores * 2
# intersection (only members in ALL sets)
ZINTERSTORE result 2 set1 set2 # only "a"
# Redis 6.2+: return without storing
ZUNION 2 set1 set2 WITHSCORES
ZINTER 2 set1 set2 WITHSCORES
ZDIFF 2 set1 set2 WITHSCORESPub/Sub
Pub/Sub Basics
Pub/Sub is fire-and-forget: messages are delivered to currently connected subscribers, with no persistence. If no subscriber is listening, the message is lost. This makes it unsuitable for reliable delivery — use Streams for that. Pub/Sub is ideal for real-time notifications, chat, and fan-out where occasional loss is acceptable. Subscribers must be connected to receive.
# subscribe to channels (in one client)
SUBSCRIBE news alerts
# publish a message to a channel (from another client)
PUBLISH news "Breaking: Redis 7.4 released"
# pattern subscription (glob-style)
PSUBSCRIBE news.*
# unsubscribe
UNSUBSCRIBE news
PUNSUBSCRIBE news.*
# note: published messages are NOT persisted
# if no subscriber is listening, the message is lost
# for persistent messaging, use Streams insteadSharded Pub/Sub (7.0+)
Sharded Pub/Sub (7.0+) solves a Cluster-mode problem: regular Pub/Sub broadcasts every PUBLISH to all cluster nodes, wasting bandwidth. Sharded Pub/Sub routes messages only to the shard that owns the channel key — far more efficient in large clusters. Use SSUBSCRIBE/SPUBLISH in Cluster deployments. The channel name determines the shard (like a key).
# regular Pub/Sub broadcasts to ALL cluster nodes (expensive)
# sharded Pub/Sub routes messages only to the shard owning the channel
# subscribe to a shard channel
SSUBSCRIBE mychannel
# publish to a shard channel
SPUBLISH mychannel "hello"
# sharded pub/sub uses the channel name for shard routing
# so messages only travel within one shard (more efficient)
# use sharded pub/sub in a Redis Cluster to avoid
# broadcasting messages to every node
# regular SUBSCRIBE/PUBLISH still work in cluster mode
# but they broadcast to all shards (cluster-wide overhead)Pub/Sub Patterns & Use Cases
Pub/Sub is perfect for fan-out patterns: one publisher, many subscribers (chat rooms, live dashboards, cache invalidation). PSUBSCRIBE with glob patterns (user:*:login) enables flexible routing. The main limitation is no persistence — if a subscriber disconnects, it misses messages. For reliable delivery, pair Pub/Sub with a Stream as a backup, or just use Streams.
# 1. Real-time chat
# client subscribes to their room
SUBSCRIBE chat:room:42
# server publishes messages
PUBLISH chat:room:42 "Alice: hello"
# 2. Cache invalidation
# subscribe to invalidation channel
SUBSCRIBE cache:invalidate
# when data changes, publish
PUBLISH cache:invalidate "user:123"
# 3. Event notifications (with PSUBSCRIBE patterns)
PSUBSCRIBE user:*:login
# matches user:1:login, user:2:login, etc.
# 4. Live dashboards
SUBSCRIBE metrics:*
# push real-time updates to dashboards
# 5. Multiplayer game state
PUBLISH game:room:1 "player_moved:5:10"Keyspace Notifications
Keyspace notifications let clients react to key lifecycle events (set, delete, expire, evict). Configure with notify-keyspace-events. They're fire-and-forget (no persistence) and not reliable — if the Redis server restarts, pending events are lost. For reliable event processing, use Streams. Keyspace notifications are great for side-effects like cleanup or cache warming.
# enable keyspace notifications (disabled by default)
CONFIG SET notify-keyspace-events "KEA"
# K = keyspace events, E = keyevent events
# g = generic (DEL, EXPIRE, ...), $ = string, l = list
# h = hash, s = set, z = sorted set, x = expired, e = evicted
# A = all except m (m = keymiss)
# subscribe to events on a specific key
SUBSCRIBE __keyspace@0__:mykey
# publishes "set", "del", "expire", etc.
# subscribe to events of a specific type
SUBSCRIBE __keyevent@0__:del
# publishes the key name that was deleted
# example: react when a key expires
SUBSCRIBE __keyspace@0__:session:abc
# receives "expired" when the key TTL endsPub/Sub vs Streams
Pub/Sub is for ephemeral real-time messaging; Streams are for durable, replayable messaging. A common hybrid: Pub/Sub for live notifications (fast, lossy) + Streams as the source of truth (durable, replayable). New subscribers read the Stream to catch up, then subscribe to Pub/Sub for live updates. For most production systems needing reliability, Streams are the better default.
# Pub/Sub: fire-and-forget, no persistence
PUBLISH channel "msg" # lost if no subscribers
# Streams: persistent, replayable, consumer groups
XADD mystream * msg "hello" # stored forever (or capped)
# Pub/Sub pros: simple, low latency, fan-out
# Pub/Sub cons: no persistence, no replay, no consumer groups
# Streams pros: persistent, replayable, consumer groups, ACK
# Streams cons: more complex, requires cleanup (XADD MAXLEN)
# when to use each:
# - real-time notifications (loss OK) -> Pub/Sub
# - chat with presence -> Pub/Sub + Streams
# - reliable task queue -> Streams
# - event sourcing / audit log -> Streams
# - fan-out to transient subscribers -> Pub/SubTransactions & Pipelining
MULTI/EXEC Transactions
MULTI/EXEC groups commands into an atomic, sequential block — no other client can interleave. Commands are queued (returning QUEUED) and executed together at EXEC. A queuing error (bad command name) aborts the whole transaction. A runtime error (wrong type on a key) skips only that command — the rest still execute. This differs from SQL transactions.
# MULTI starts a transaction; commands are queued
MULTI
SET counter 1
INCR counter
INCR counter
GET counter
EXEC
# returns: [OK, 2, 3, "3"]
# discard a transaction (cancels all queued commands)
MULTI
SET x 1
DISCARD
# errors during queuing (syntax) abort the whole transaction
# errors during EXEC (e.g., wrong type) skip only that commandWATCH (Optimistic Locking)
WATCH implements optimistic locking: if a watched key changes between WATCH and EXEC, the transaction is aborted (EXEC returns nil). This is the standard Redis pattern for read-modify-write atomicity (since Redis has no row locks). Always retry on nil. WATCH is checked at EXEC time, not during queuing. UNWATCH cancels watches (also happens on DISCONNECT).
# WATCH monitors keys; if any change before EXEC, the transaction aborts
WATCH counter
val = GET counter # read current value
MULTI
INCR counter # queue the update
EXEC # nil if counter changed -> retry
# typical pattern: read-modify-write with retry
WATCH key
current = GET key
new_value = compute(current)
MULTI
SET key new_value
EXEC
# if EXEC returns nil, the key changed — loop and retry
# UNWATCH cancels all watches
UNWATCHPipelining (Batch Round-Trips)
Pipelining batches commands to reduce network round-trips — dramatically faster than individual commands over a network. Unlike MULTI/EXEC, pipelined commands are NOT atomic (other clients can interleave). Use pipelining for bulk operations where atomicity doesn't matter (bulk inserts, reads) and MULTI/EXEC when you need atomicity. Most clients pipeline automatically within MULTI/EXEC.
# pipelining sends multiple commands in one network round-trip
# (no atomicity guarantee, just reduced latency)
# in a client library (e.g., Node.js / redis):
const pipe = client.multi()
pipe.set("k1", "v1")
pipe.set("k2", "v2")
pipe.incr("counter")
const results = await pipe.exec()
# vs MULTI/EXEC: pipelining does NOT guarantee atomicity
# other clients can interleave between pipelined commands
# raw protocol example (RESP):
# *1\r\n$4\r\nPING\r\n*1\r\n$4\r\nPING\r\n
# (send multiple commands at once, read multiple replies)Lua Scripts (Atomic)
Lua scripts are the Redis way to do complex atomic operations — the script runs as a single atomic unit (no interleaving). EVALSHA re-runs a loaded script by hash (avoids resending the script). Keep scripts short (they block the server). Scripts have deterministic requirements (no random/time calls) for replication. Redis 7+ uses FUNCTIONS as a more structured alternative.
# Lua scripts run atomically — no other command runs during execution
# EVAL: run a script
EVAL "return redis.call('GET', KEYS[1])" 1 mykey
# a check-and-set script:
EVAL "
local cur = redis.call('GET', KEYS[1])
if cur == ARGV[1] then
return redis.call('SET', KEYS[1], ARGV[2])
end
return 0
" 1 mykey "expected" "newvalue"
# load a script once, then run by SHA hash (efficient)
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# returns a SHA1 hash
EVALSHA <sha1> 1 mykey
# check if a script is loaded
SCRIPT EXISTS <sha1>Functions (Redis 7.0+)
Functions (7.0+) improve on EVAL scripts: they're named (not just hashed), organized into libraries, and persist across restarts (EVAL scripts are ephemeral). This makes them easier to manage and deploy. FCALL invokes a function by name. Functions are the modern way to add atomic server-side logic. Like scripts, they must be deterministic for replication.
# FUNCTIONS are named, versioned Lua scripts (better than EVAL)
# register a function library
FUNCTION LOAD '#!lua name=mylib
redis.register_function("my_set",
function(keys, args)
return redis.call("SET", keys[1], args[1])
end
)'
# call a function by name
FCALL my_set 1 mykey "hello"
# list loaded libraries
FUNCTION LIST
# get the function's code
FUNCTION DUMP mylib
# delete a library
FUNCTION DELETE mylib
# functions persist across restarts (unlike EVAL scripts)Persistence
RDB Snapshots
RDB snapshots are compact binary files ideal for backups and disaster recovery. BGSAVE forks the process (copy-on-write) so the main thread isn't blocked, but large datasets can still cause memory spikes during fork. The 'save' rules trigger automatic snapshots based on change rate. RDB risks losing data written after the last snapshot — pair with AOF for durability.
# RDB: point-in-time snapshot of the dataset (compact binary)
# trigger a snapshot manually:
SAVE # blocks until done (use with care)
BGSAVE # forks a background process (non-blocking)
LASTSAVE # Unix timestamp of the last successful save
# configure automatic snapshots in redis.conf:
# save 3600 1 # save if >= 1 key changed in 3600s
# save 300 100 # save if >= 100 keys changed in 300s
# save 60 10000 # save if >= 10000 keys changed in 60s
# save "" # disable RDB entirely
# RDB file location (default: dump.rdb in the working dir)
CONFIG GET dbfilename
CONFIG GET dir
# RDB pros: compact, fast restart, great for backups
# RDB cons: potential data loss between snapshotsAOF (Append-Only File)
AOF appends every write command to a log — far more durable than RDB (max 1 second of data loss with everysec). 'always' fsyncs on every write (very slow, use only for critical data). BGREWRITEAOF compacts the log (rewrites it as the minimal set of commands). AOF files are larger and slower to load than RDB. Redis 7 uses a multi-part AOF format (base + incremental).
# AOF: logs every write command (durable, replayable)
# enable in redis.conf:
# appendonly yes
# appendfilename "appendonly.aof"
# fsync policies:
# appendfsync always # fsync every write (safest, slowest)
# appendfsync everysec # fsync once per second (default, balanced)
# appendfsync no # let OS decide (fastest, risk of data loss)
# trigger AOF rewrite (compacts the log)
BGREWRITEAOF
# AOF auto-rewrite thresholds:
# auto-aof-rewrite-percentage 100
# auto-aof-rewrite-min-size 64mb
# AOF pros: minimal data loss (1 sec max with everysec)
# AOF cons: larger files, slower load on restartRDB + AOF Hybrid
Running RDB + AOF together gives you RDB's fast restart and AOF's durability. Since Redis 7, the AOF is a multi-part file: an RDB base snapshot + incremental command log — combining fast load with minimal data loss. aof-use-rdb-preamble (default yes) puts an RDB snapshot at the start of the AOF for faster recovery. This is the recommended production configuration.
# use BOTH RDB and AOF for the best of both worlds
# redis.conf:
# save 900 1 # RDB snapshots
# appendonly yes # AOF enabled
# aof-use-rdb-preamble yes # RDB header in AOF (faster load)
# on restart, Redis loads AOF (more complete)
# the AOF file can start with an RDB snapshot (faster load)
# followed by incremental AOF commands
# disaster recovery: keep both RDB and AOF backups
# RDB for fast restores, AOF for minimal data loss
# check what's enabled
CONFIG GET save
CONFIG GET appendonly
# monitor persistence operations
INFO persistenceBackup & Restore
Back up the RDB file (compact, fast) regularly — it's a consistent snapshot even while Redis runs (copy-on-write). For AOF, back up the whole appendonly directory (Redis 7 multi-part). Restore by stopping Redis, replacing files, and restarting. To recover from a bad command, edit the AOF to remove it before restart. Always test backups with redis-check-rdb/aof.
# backup: copy the RDB file while Redis runs
cp /var/lib/redis/dump.rdb /backup/dump-$(date +%F).rdb
# or trigger a BGSAVE first for a consistent snapshot
BGSAVE
# wait for LASTSAVE to update, then copy the file
# AOF backup: copy the AOF manifest + files
cp /var/lib/redis/appendonly.aof.* /backup/
# restore: stop Redis, replace the data files, start
redis-cli SHUTDOWN
cp /backup/dump.rdb /var/lib/redis/dump.rdb
redis-server /etc/redis/redis.conf
# for point-in-time recovery from AOF:
# edit the AOF file to remove unwanted commands, then restart
# RDB file analysis (offline)
redis-check-rdb /var/lib/redis/dump.rdb
redis-check-aof /var/lib/redis/appendonly.aofPersistence Trade-offs
Persistence choice depends on your durability needs. Pure cache: disable persistence for max speed. Critical data: RDB+AOF (recommended). A common pattern: disable persistence on the primary (for performance) and persist on a replica (offloads disk I/O). Everysec AOF is the sweet spot — at most 1 second of data loss with minimal performance impact. Always test recovery.
# choosing a persistence strategy:
# 1. RDB only (cache, re-buildable data)
# save 3600 1
# appendonly no
# -> fast restart, some data loss on crash
# 2. AOF only (durability-critical, everysec)
# save ""
# appendonly yes
# appendfsync everysec
# -> up to 1s data loss, larger files
# 3. RDB + AOF (recommended production default)
# save 3600 1
# appendonly yes
# aof-use-rdb-preamble yes
# -> durability + fast restart
# 4. No persistence (pure cache, ephemeral)
# save ""
# appendonly no
# -> maximum speed, all data lost on restart
# 5. Replica-only persistence (offload from primary)
# primary: no persistence (max performance)
# replica: RDB + AOF (persist here instead)Clustering
Redis Cluster Basics
Redis Cluster shards data across nodes using 16,384 hash slots. CRC16(key) % 16384 determines the slot. Multi-key operations (MGET, transactions) require keys in the same slot — use hash tags ({user:1}) to force related keys together. Each primary can have replicas for HA. The cluster handles failover automatically when a primary fails.
# Redis Cluster shards data across multiple nodes (16,384 slots)
# each key maps to a slot: CRC16(key) % 16384
# create a cluster (6 nodes: 3 primaries + 3 replicas)
redis-cli --cluster create \
host1:7000 host2:7000 host3:7000 \
host1:7001 host2:7001 host3:7001 \
--cluster-replicas 1
# check cluster status
redis-cli -p 7000 CLUSTER INFO
redis-cli -p 7000 CLUSTER NODES
# count slots assigned
CLUSTER COUNTKEYSINSLOT 1234
# get the slot for a key
CLUSTER KEYSLOT mykey # e.g., 1234
# multi-key operations require keys in the SAME slot
# use hash tags to force keys to the same slot:
SET {user:1}:profile "alice"
SET {user:1}:cart "items"
# both map to the same slot (the part inside {})Hash Tags & Multi-Key Ops
Hash tags ({...}) force keys to the same slot, enabling multi-key operations in a cluster. But use them judiciously — over-using one tag (e.g., {users}:1, {users}:2) creates a hotspot on one shard. Design hash tags around access patterns: group keys accessed together (a user's data, an order's items) but keep unrelated keys separate. This is the core cluster data-modeling challenge.
# hash tags: the substring inside {} determines the slot
# {user:1}:profile and {user:1}:cart share a slot
# enabling multi-key operations:
MSET {user:1}:name "Alice" {user:1}:age 30 # OK (same slot)
MSET user:1:name "Alice" user:2:age 30 # CROSSSLOT error
# transactions across keys (must be same slot)
WATCH {order:1}:items
MULTI
HINCRBY {order:1}:items product:1 1
HINCRBY {order:1}:total 10
EXEC
# Lua scripts accessing multiple keys (all keys must share a slot)
EVAL "..." 2 {user:1}:a {user:1}:b
# design hash tags carefully:
# - too broad -> hotspots (one shard overloaded)
# - too narrow -> can't do multi-key opsCluster Resharding
Resharding moves slots between nodes without downtime — Redis migrates keys one at a time while serving requests. Add nodes then rebalance to spread load. The --cluster CLI tool handles the complex orchestration. Always run --cluster check after changes. Resharding is slow (key-by-key migration), so plan capacity ahead rather than resharding under load.
# move slots from one node to another (resharding)
redis-cli --cluster reshard host1:7000
# prompts for: how many slots, target node, source nodes
# add a new node to the cluster
redis-cli --cluster add-node newhost:7000 host1:7000
# add a new replica to an existing primary
redis-cli --cluster add-node newhost:7001 host1:7000 \
--cluster-slave --cluster-master-id <nodeId>
# remove a node (migrates its slots first)
redis-cli --cluster del-node host1:7000 <nodeId>
# rebalance slots across all nodes
redis-cli --cluster rebalance host1:7000
# check cluster health
redis-cli --cluster check host1:7000
redis-cli --cluster fix host1:7000 # repair issuesCluster Failover
Cluster failover is automatic: if a primary is unreachable beyond cluster-node-timeout (default 15s), its replica is promoted. Manual failover is for maintenance (zero-downtime upgrades). TAKEOVER risks split-brain — use only in emergencies. Without replicas, a failing primary makes its slots unavailable (cluster down). Always run with replicas in production. CLUSTER NODES shows the topology.
# when a primary fails, its replica is promoted automatically
# manual failover (for maintenance):
redis-cli -p 7001 CLUSTER FAILOVER # replica -> primary
redis-cli -p 7001 CLUSTER FAILOVER FORCE # without primary agreement
redis-cli -p 7001 CLUSTER FAILOVER TAKEOVER # force, may split brain
# view the cluster topology
CLUSTER NODES
CLUSTER SHARDS # (7.0+)
# a node flagged as FAIL after timeout is removed
# cluster- node-timeout 15000 (15 seconds in redis.conf)
# if a cluster has no replica for a failing primary,
# that slot range becomes unavailable until the primary returns
# (or you manually fix it)
# CLUSTER RESET resets a node (HARD removes all data)Cluster vs Sentinel vs Single
Single instance for dev. Sentinel for HA when data fits on one machine (one primary + replicas + sentinels for monitoring/failover). Cluster for horizontal scaling when data exceeds one machine or write throughput is very high. Cluster adds complexity (hash tags, multi-key limits). Managed services (Redis Cloud, ElastiCache, MemoryDB) handle the operational burden — prefer them unless you have strong reasons to self-host.
# Single Redis: one instance, no HA, no scaling
# simplest, fine for dev/small cache
# Sentinel: 1 primary + replicas + sentinel monitors
# HA (automatic failover), no sharding
# good when dataset fits on one machine
# sentinel.conf: sentinel monitor mymaster host 6379 2
# Cluster: sharded across N primaries + replicas
# HA + horizontal scaling
# good for large datasets / high throughput
# more complex (multi-key limitations)
# choose:
# - dev / tiny cache -> single
# - production, data fits in RAM -> sentinel (HA)
# - data > single machine RAM -> cluster (sharding)
# - very high write throughput -> cluster (shard writes)
# managed Redis (Redis Cloud, ElastiCache) handles
# sentinel/cluster ops for youSentinel (High Availability)
Sentinel Setup
Sentinel provides HA without sharding: it monitors a primary, promotes a replica on failure, and reconfigures clients. Quorum (the last number) is how many sentinels must agree a primary is down — set to (N/2)+1 for N sentinels. Deploy 3 sentinels on separate machines for fault tolerance. down-after-milliseconds should be > network jitter; too low causes false failovers.
# Sentinel monitors a primary + replicas, handles failover
# sentinel.conf (port 26379):
sentinel monitor mymaster 192.168.1.10 6379 2
# name, host, port, quorum (number of sentinels to agree on failure)
sentinel down-after-milliseconds mymaster 30000 # 30s to declare down
sentinel failover-timeout mymaster 180000 # 3min failover timeout
sentinel parallel-syncs mymaster 1 # replicas to resync in parallel
# start a sentinel:
redis-sentinel /etc/redis/sentinel.conf
# or: redis-server /etc/redis/sentinel.conf --sentinel
# deploy at least 3 sentinels (odd number) for quorum
# sentinels communicate with each other and the Redis nodes
# connect to a sentinel to find the current primary:
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymasterSentinel Commands
Clients connect to Sentinels to discover the current primary, then connect to it. On failover, clients query Sentinels again to find the new primary. SENTINEL failover triggers a manual failover (for rolling upgrades). ckquorum verifies you have enough sentinels for a quorum — run this in monitoring. All major Redis client libraries support Sentinel natively.
# query sentinel state
redis-cli -p 26379 SENTINEL masters # all monitored primaries
redis-cli -p 26379 SENTINEL master mymaster # details of one primary
redis-cli -p 26379 SENTINEL replicas mymaster
redis-cli -p 26379 SENTINEL sentinels mymaster
# find the current primary address
SENTINEL get-master-addr-by-name mymaster
# force a failover (manual, for maintenance)
SENTINEL failover mymaster
# reset a master's state (clears replicas/sentinels discovered)
SENTINEL reset mymaster
# check the number of OK sentinels
SENTINEL ckquorum mymaster
# "OK 3 usable Sentinels" or "NOQUORUM" warning
# clients should connect to sentinels, NOT directly to the primary
# libraries: redis-py, jedis, ioredis all support sentinel modeFailover Process
Failover is automatic but causes a brief write outage (seconds). SDOWN is one sentinel's opinion; ODOWN is quorum consensus. The elected sentinel leader picks the most up-to-date replica to promote. When the old primary returns, it becomes a replica — writes it accepted during the network partition are LOST (split-brain risk). Subscribe to +switch-master for alerts.
# when a primary is down longer than down-after-milliseconds:
# 1. sentinels mark it SDOWN (subjectively down)
# 2. quorum agrees -> ODOWN (objectively down)
# 3. a sentinel is elected leader
# 4. leader picks the best replica (most up-to-date)
# 5. promotes the replica: REPLICAOF NO ONE
# 6. reconfigures other replicas to follow the new primary
# 7. updates sentinel configs with the new primary address
# 8. clients reconnect via sentinel discovery
# during failover, writes fail briefly (seconds)
# reads from replicas continue (if the app uses them)
# the old primary, when it returns, is reconfigured as a replica
# of the new primary (its writes during partition are lost)
# monitor failover events:
redis-cli -p 26379 SUBSCRIBE +switch-master
redis-cli -p 26379 SUBSCRIBE +failover-*Read Replicas & Scaling
Replicas offload reads from the primary — useful for read-heavy workloads (analytics, reporting). Replication is asynchronous, so replica data may lag (milliseconds normally, but can grow under load). Use READONLY to read from replicas. Writable replicas (4.0+) accept non-replicated writes — useful for temp data or per-replica caches, but writes are lost on failover. Monitor replica lag.
# replicas can serve reads (eventually consistent)
REPLICAOF 192.168.1.10 6379 # become a replica of this primary
REPLICAOF NO ONE # promote to primary (manual)
# check replication status
INFO replication
# role:slave, master_host, master_link_status:up, ...
# replica lag (bytes and seconds lagging behind primary)
# master_repl_offset vs slave_repl_offset
# clients can route reads to replicas for read scaling
# but beware: replica data may be stale (replication lag)
# Redis Sentinel + clients: configure read preference
# READONLY command enables reads on a replica
# writable replicas (Redis 4.0+): accept writes that are
# local-only (not replicated) — useful for ephemeral data
# replica-read-only no (in redis.conf)Replication Internals
Replication is async and one-way (primary to replica). Initial sync sends a full RDB then the command backlog; PSYNC enables partial resync after short disconnects (using the replication backlog — tune repl-backlog-size). A replication ID change (on primary restart or failover) forces a full resync. Chain replication (replica of a replica) reduces primary load for large fan-out.
# replication is asynchronous: primary -> replica (one-way)
# initial sync: full RDB transfer + buffered commands
# subsequent: streaming command log
# trigger a full resync (force replica to reload everything)
redis-cli -p 6380 REPLICAOF NO ONE
redis-cli -p 6380 REPLICAOF 192.168.1.10 6379
# partial resync (PSYNC): if the replica disconnects briefly,
# it can resume from the replication backlog (no full resync)
# repl-backlog-size 1mb (tune for longer disconnects)
# monitor replication in real-time
redis-cli -p 6379 INFO replication
# master_repl_offset, slave_read_repl_offset, backlog_size
# chain replication: a replica of a replica (tree topology)
# reduces load on the primary for many replicas
REPLICAOF replica-host 6380 # chain off another replica
# replication ID changes on every primary restart -> full resyncStreams
Stream Basics (XADD/XREAD)
Streams are Redis's durable messaging structure — append-only, replayable, with consumer groups. IDs are timestamp-based (auto-generated with *). XREAD with $ tails the stream (new entries only); with 0 reads from the start. MAXLEN caps the stream (~ for approximate is much faster). Streams solve Pub/Sub's no-persistence problem and are the recommended way to build reliable queues.
# streams are append-only logs with IDs (durable, replayable)
# XADD: add an entry (auto-generate ID with *)
XADD mystream * sensor temp 25.5
# returns an ID like "1700000000000-0" (timestamp-sequence)
# add with a specific ID (must be greater than the last)
XADD mystream 1700000001000-0 sensor temp 26.0
# read entries
XREAD COUNT 10 STREAMS mystream 0 # from the beginning
XREAD COUNT 10 STREAMS mystream $ # new entries only (tail)
# block for new entries (like BLPOP for lists)
XREAD BLOCK 30000 STREAMS mystream $ # blocks up to 30s
# get the length
XLEN mystream # number of entries
# capped stream (keep only last N entries)
XADD mystream MAXLEN 1000 * field value
XADD mystream MAXLEN ~ 1000 * field value # ~ = approx (faster)Consumer Groups
Consumer groups enable parallel processing: each entry is delivered to exactly one consumer in the group. '>' reads new entries; '0' reads this consumer's pending (unacked) entries. XACK marks an entry processed. Pending entries (delivered but not acked) form a processing log — use XPENDING to find stuck messages and XCLAIM to reassign them to another consumer after a timeout.
# create a consumer group at a specific position
XGROUP CREATE mystream mygroup $ # only new entries
XGROUP CREATE mystream mygroup 0 # all existing entries
XGROUP CREATE mystream mygroup 0 MKSTREAM # create stream if missing
# read entries as a consumer (assigns pending entries)
XREADGROUP GROUP mygroup consumer1 COUNT 10 STREAMS mystream >
# '>' = never-delivered entries
# read pending entries (for this consumer, not yet acked)
XREADGROUP GROUP mygroup consumer1 STREAMS mystream 0
# acknowledge processing (removes from pending)
XACK mystream mygroup <id1> <id2>
# view pending entries (pending entries list)
XPENDING mystream mygroup
# returns: count, min-id, max-id, consumers
# view a consumer's pending entries
XPENDING mystream mygroup - + 10 consumer1Stream Processing & Recovery
XAUTOCLAIM is key for reliable processing: it reassigns entries that have been pending too long (consumer crashed or stalled) to a healthy consumer. The min-idle-time prevents stealing entries still being processed. XTRIM bounds stream size (essential for memory). XRANGE reads a range by ID. XINFO reveals stream/group/consumer state for monitoring. This is a complete, durable message queue.
# XCLAIM: reassign a pending entry to another consumer
# (after the original consumer died or is too slow)
XCLAIM mystream mygroup consumer2 60000 <entry-id>
# 60000 = min idle time (ms) before claiming
# XAUTOCLAIM (6.2+): auto-claim stale pending entries
XAUTOCLAIM mystream mygroup consumer2 60000 0 COUNT 10
# claims entries idle > 60s, returns them to consumer2
# view stream info
XINFO STREAM mystream FULL
XINFO GROUPS mystream
XINFO CONSUMERS mystream mygroup
# delete an entry (rarely needed)
XDEL mystream <id>
# trim the stream (free memory)
XTRIM mystream MAXLEN 10000
XTRIM mystream MINID 1700000000000 # remove older than this ID
# range queries
XRANGE mystream - + # all entries
XRANGE mystream 1700000000000 - 1700000009999 -Stream Use Cases
Streams excel at reliable queues, event sourcing, and time-series data. The consumer-group pattern (XADD + XREADGROUP + XACK) is a complete replacement for list-based queues, with built-in retry (pending entries) and recovery (XAUTOCLAIM). For time-series, MAXLEN keeps memory bounded. Streams are the most feature-complete Redis structure — use them whenever you need durability and ordered processing.
# 1. Reliable task queue (replaces lists + BRPOPLPUSH)
XADD tasks * type email payload '{"to":"[email protected]"}'
XREADGROUP GROUP workers worker1 COUNT 1 STREAMS tasks >
# process and XACK on success
# 2. Event sourcing / audit log
XADD events * user 1 action login ip 1.2.3.4
# replay with XRANGE for reconstruction
# 3. Real-time analytics (time-windowed)
XADD metrics * cpu 75 mem 60
XRANGE metrics - + # query by time range
# 4. Chat history (capped)
XADD chat:room:1 MAXLEN ~ 1000 * user alice msg "hello"
XREAD COUNT 20 STREAMS chat:room:1 0
# 5. IoT sensor data (with TTL via trimming)
XADD sensors:temp MAXLEN ~ 86400 * value 22.5 # keep last dayStreams vs Other Queues
Streams are the Redis-native queue — simpler than Kafka/RabbitMQ and sufficient for most workloads that fit in RAM. They lack Kafka's partition-level parallelism (one stream = one shard) but consumer groups provide worker parallelism. For very high throughput or disk-based durability, Kafka wins. For complex routing, RabbitMQ wins. For most apps already using Redis, Streams are the right default for reliable messaging.
# Redis Streams vs Lists (for queues):
# Lists: LPUSH/BRPOP, simple, NO retry/recovery
# Streams: consumer groups, ACK, pending, XAUTOCLAIM
# Redis Streams vs Kafka:
# Streams: in-memory (RAM-bounded), simpler, single-node or cluster
# Kafka: disk-based, partitioned, higher throughput at scale
# Redis Streams vs RabbitMQ:
# Streams: simpler, fewer features, in-memory
# RabbitMQ: rich routing, exchanges, acknowledgments, disk
# Streams capacity: limited by RAM (use MAXLEN/MINID to trim)
# Streams persistence: RDB/AOF (like any Redis data)
# choose Streams when:
# - you need a queue but already use Redis
# - dataset fits in RAM
# - you want simplicity over Kafka's scale
# - you need consumer groups with retryHyperLogLog, Bitmaps & Geo
HyperLogLog (Cardinality)
HyperLogLog (HLL) counts unique elements with ~0.81% error using a fixed 12KB — count billions of items with constant memory. Perfect for analytics like daily/weekly/monthly unique visitors. PFMERGE combines HLLs (e.g., monthly uniques from daily). The trade-off is approximation — use a Set when you need exact counts or membership tests (SISMEMBER), HLL when you only need the count.
# HyperLogLog: approximate unique count with fixed 12KB memory
# perfect for counting unique visitors, IPs, etc. at scale
# add elements (returns 1 if the cardinality changed)
PFADD visitors:2025-01-01 "user:1" "user:2" "user:3"
# get the approximate unique count
PFCOUNT visitors:2025-01-01 # ~3
# merge multiple HLLs (e.g., monthly uniques from daily)
PFMERGE visitors:2025-01 visitors:2025-01-01 visitors:2025-01-02
# accuracy: ~0.81% standard error
# memory: always ~12KB regardless of element count (up to 2^64)
# vs a Set for unique counting:
# SADD + SCARD: exact, but O(N) memory (grows with count)
# PFADD + PFCOUNT: approximate, fixed 12KB memory
# 1 million uniques: Set ~10MB, HLL always 12KBBitmaps Deep Dive
Bitmaps store one bit per user — 1 million users = 125KB, 4 billion = 512MB. BITOP AND/OR/XOR/NOT combine bitmaps (retention, churn analysis). BITPOS finds the first set bit. Use cases: daily active users, feature flags, attendance, A/B test groups. Limitation: requires dense integer user IDs (gaps waste space). For sparse data, prefer a Set or HyperLogLog.
# bitmaps: boolean operations on a string's bits
# ideal for per-user flags over a large user base (user ID = bit position)
# mark user 7 as "active today"
SETBIT active:2025-01-01 7 1
# check if user 7 was active
GETBIT active:2025-01-01 7 # 1
# count active users
BITCOUNT active:2025-01-01
# users active on BOTH days (AND)
BITOP AND active:both active:2025-01-01 active:2025-01-02
BITCOUNT active:both
# users active on EITHER day (OR)
BITOP OR active:either active:2025-01-01 active:2025-01-02
# users active on day 1 but NOT day 2 (XOR or AND NOT)
BITOP XOR diff active:2025-01-01 active:2025-01-02
# find the first active user
BITPOS active:2025-01-01 1 # position of first 1-bit
# memory: 1 million users = 125KB (1 bit per user)Geospatial (GEO)
GEO commands store geographic points and support radius/box queries — built on sorted sets (score = geohash). GEOSEARCH (6.2+, replaces GEORADIUS) finds nearby points with rich options (WITHCOORD, WITHDIST, ASC/DESC). Distance is computed on a sphere. Precision is good for most apps. For complex GIS (polygons, routing), use a dedicated spatial database. GEO is perfect for 'find nearby' features.
# GEO: built on sorted sets, stores (lng, lat, name) points
# add locations (longitude, latitude, name)
GEOADD places -73.99 40.73 "Park" -73.98 40.75 "Museum" -74.00 40.70 "Cafe"
# get coordinates of a member
GEOPOS places "Park" # [["-73.99","40.73"]]
# distance between two members (meters by default)
GEODIST places "Park" "Museum" # "1623.5"
GEODIST places "Park" "Museum" km # "1.623"
# find members within a radius (meters)
GEOSEARCH places FROMMEMBER "Park" BYRADIUS 1000 m ASC
# members within 1km of Park, sorted by distance
# find members within a bounding box
GEOSEARCH places FROMLONLAT -73.99 40.73 BYBOX 2 2 km
# GEOSEARCH also returns distance and coordinates
GEOSEARCH places FROMMEMBER "Park" BYRADIUS 500 m WITHCOORD WITHDISTGeospatial Operations
GEO is implemented on sorted sets — geohash of coordinates is the score. This means you can use Z* commands for some operations (ZREM, ZRANGE). GEOSEARCH returns rich results (distance, coordinates, geohash). COUNT limits results (use with ASC for nearest-N). GEOSEARCHSTORE saves results for further processing. Geohash precision degrades near the poles — fine for most real-world apps.
# add multiple points at once
GEOADD cities -122.41 37.78 "San Francisco" -74.00 40.71 "New York"
# get the geohash of a member
GEOHASH places "Park" # ["dr5reg..."]
# remove a member (uses ZREM internally)
ZREM places "Cafe"
# count members in an area (using ZCOUNT on the underlying zset)
ZCOUNT places -180 180 # all members
# GEOSEARCH with a center point and limiting results
GEOSEARCH places FROMLONLAT -73.99 40.73 \
BYRADIUS 2000 m ASC COUNT 5 # nearest 5 within 2km
# GEOSEARCHSTORE: store results in a new key (6.2+)
GEOSEARCHSTORE nearby FROMLONLAT -73.99 40.73 BYRADIUS 1000 m
# the underlying structure is a sorted set, so:
ZRANGE places 0 -1 # list all members
ZSCORE places "Park" # geohash scoreChoosing Specialized Types
Redis offers many specialized structures beyond the core five. HyperLogLog for approximate cardinality, Bitmaps for dense boolean flags, GEO for proximity. Modules add Bloom filters (membership test with false positives), Top-K (heavy hitters), and RedisTimeSeries (optimized time-series). Match the structure to your data shape and accuracy needs — the right choice can save orders of magnitude of memory.
# Cardinality counting (unique elements):
# exact, need membership test -> Set (SADD/SISMEMBER/SCARD)
# approximate, huge scale -> HyperLogLog (PFADD/PFCOUNT)
# per-time-bucket uniques -> HLL per day, PFMERGE for ranges
# Boolean flags over a dense ID space:
# small number of flags -> Set of active IDs
# millions of users -> Bitmap (SETBIT/BITCOUNT)
# multiple counters compactly -> BITFIELD
# Geographic points:
# "find nearby" queries -> GEO (GEOADD/GEOSEARCH)
# complex GIS -> use PostGIS/external
# Time-series data:
# simple, capped -> Stream (XADD MAXLEN)
# dedicated TS module -> RedisTimeSeries (module)
# Probabilistic structures (modules):
# Bloom filter -> RedisBloom (BF.ADD/BF.EXISTS)
# Top-K -> RedisBloom (TOPK)Security
Authentication & ACLs
Redis 6+ ACLs replace the single password model — create users with scoped permissions. The syntax: on (enabled), >password, ~pattern (key patterns), +command/-command, +@category/-@category (e.g., +@read, -@dangerous). Always create a least-privilege user for each app. Save ACLs to a file for persistence. Never use the default user in production — disable it or set a strong password.
# set a password (simple, legacy)
CONFIG SET requirepass "strongpassword"
AUTH "strongpassword"
# Redis 6+: ACL system with users and permissions
# add a user with limited access
ACL SETUSER appuser on >secretpassword ~app:* +get +set +del -@dangerous
# list users
ACL LIST
ACL WHOAMI # current user
# view a user's permissions
ACL GETUSER appuser
# delete a user
ACL DELUSER appuser
# save ACLs to a file (persists across restarts)
ACL SAVE
ACL LOAD # reload from file
# aclfile in redis.conf:
# aclfile /etc/redis/users.aclTLS/SSL Encryption
TLS encrypts data in transit — essential for production, especially over untrusted networks. Redis 6+ supports TLS natively. mTLS (tls-auth-clients yes) requires clients to present a certificate, providing mutual authentication without passwords. For cluster/sentinel, enable TLS on inter-node communication too. Port 0 disables plain-text; during migration, you can run both ports temporarily.
# enable TLS in redis.conf (Redis 6+)
# generate certs first (or use a real CA):
port 0 # disable plain-text port
tls-port 6379
tls-cert-file /etc/redis/redis.crt
tls-key-file /etc/redis/redis.key
tls-ca-cert-file /etc/redis/ca.crt
# connect with TLS
redis-cli --tls --cert client.crt --key client.key \
--cacert ca.crt -h host -p 6379
# mutual TLS (mTLS) for client authentication
tls-cluster yes # TLS between cluster nodes
tls-replication yes # TLS between primary/replica
tls-auth-clients yes # require client certs
# mix TLS and plain text (transition period)
# port 6380
# tls-port 6379Network Security
Redis has no native network access control — use bind + protected-mode + a firewall. NEVER expose Redis to the internet (countless breaches from open Redis with no auth). protected-mode blocks external connections when no password is set. rename-command disables dangerous commands (FLUSHALL, CONFIG, KEYS), but ACLs (6.0+) are the modern approach. Put Redis in a private subnet; access via a VPN or bastion.
# bind to specific interfaces (never 0.0.0.0 in prod)
bind 127.0.0.1 10.0.0.5
protected-mode yes # blocks external access if no password
# require authentication
requirepass "strong-random-password"
# rename dangerous commands (or disable them)
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG "CONFIG_9f8a7b"
rename-command KEYS ""
# Redis 6+: better to use ACLs instead of rename-command
# ACL SETUSER default -flushall -config -keys
# disable debug commands
# ACL SETUSER default -@dangerous
# use a firewall / security group to restrict access
# Redis should NEVER be directly internet-accessible
# (the 2018+ ransom attacks scanned for open Redis instances)Audit & Logging
SLOWLOG records slow commands for performance auditing — set the threshold to your SLO (e.g., 10ms). MONITOR logs every command (huge performance impact — debug only). LATENCY MONITOR helps find latency spikes. CLIENT LIST shows connected clients (watch for leaks). Set loglevel to 'notice' in production (debug is too verbose). Ship logs to a central system for analysis.
# log slow commands (slowlog)
CONFIG SET slowlog-log-slower-than 10000 # 10ms in microseconds
CONFIG SET slowlog-max-len 128 # keep 128 entries
SLOWLOG GET 10 # last 10 slow commands
SLOWLOG RESET
# log to a file (redis.conf)
logfile /var/log/redis/redis.log
loglevel notice # debug|verbose|notice|warning
# monitor all commands (debugging only — huge perf impact)
MONITOR
# latency monitoring
CONFIG SET latency-monitor-threshold 100 # 100ms
LATENCY HISTORY event-name
LATENCY DOCTOR # analysis report
# client list (who's connected)
CLIENT LIST
CLIENT GETNAME
CLIENT SETNAME "my-app"Security Best Practices
Redis security is layered: network (bind/firewall), authentication (password/ACL), transport (TLS), command restriction (ACL/rename), and audit (slowlog/monitor). The #1 rule: never expose Redis directly to the internet — it has no built-in rate limiting or brute-force protection. Encrypt RDB/AOF files at the disk level (they contain your data in plaintext). Keep Redis updated for security patches.
# 1. NEVER expose Redis to the public internet
bind 127.0.0.1
protected-mode yes
# 2. Use strong passwords or ACLs
requirepass "<64-char-random-string>"
# or ACLs with least privilege
# 3. Enable TLS for transit encryption
tls-port 6379
# 4. Restrict dangerous commands
ACL SETUSER default -@dangerous -flushall -keys
# 5. Use a firewall / security group
# allow only app servers on the Redis port
# 6. Rotate credentials regularly
ACL SETUSER appuser on >newpassword ~app:* +@read +@write
# 7. Audit access
LOGICALDB MONITOR # or use an audit module
# 8. Encrypt sensitive data at rest (disk encryption)
# RDB/AOF files can contain sensitive data
# 9. Keep Redis updated (security patches)
# 10. Isolate environments (separate dev/staging/prod)Performance & Optimization
Performance Fundamentals
Redis is single-threaded for command execution — one slow command blocks ALL clients. Never use KEYS, SMEMBERS, or DEL on large structures in production. Use SCAN-family, HSCAN, and UNLINK (async delete) instead. Pipeline commands to reduce round-trips. Know the Big-O of each command: O(1) and O(log N) are safe; O(N) commands should be bounded (LRANGE with small ranges).
# Redis is single-threaded (mostly) — avoid blocking commands
# DANGEROUS on large datasets (block the server):
KEYS * # use SCAN instead
SMEMBERS bigset # use SSCAN
HGETALL bighash # use HSCAN
DEL hugekey # use UNLINK (async delete)
FLUSHDB # use FLUSHDB ASYNC
# use UNLINK instead of DEL for large keys (non-blocking)
UNLINK bigkey # deletes in a background thread
# favor pipelining and batch commands
MGET k1 k2 k3 # one round-trip, not three GETs
LPUSH list a b c # one command, not three
# prefer structurally efficient commands
# O(1): GET, SET, HGET, SISMEMBER, ZSCORE
# O(log N): ZADD, ZRANGE
# O(N): LRANGE, SMEMBERS, KEYS, SORTPipelining & Batch Operations
Pipelining is the single biggest Redis performance lever — batching commands into one network round-trip can give 10-100x speedup. Each round-trip costs network latency (~0.1-1ms); 1000 individual commands = 100-1000ms, but pipelined = ~1ms. Don't over-pipeline (queued replies consume memory). Benchmark with redis-benchmark -P to find your sweet spot. Transactions use pipelining automatically.
# pipelining: send many commands without waiting for replies
# (reduces round-trip time — huge speedup over network)
# example (Node.js with ioredis):
const pipeline = redis.pipeline()
for (let i = 0; i < 1000; i++) {
pipeline.set(`key:${i}`, "value")
}
await pipeline.exec() # one round-trip for 1000 commands
# transactions (MULTI/EXEC) are automatically pipelined
redis.multi().set("k", "v").incr("c").exec()
# MSET/MGET batch in one command
MSET k1 v1 k2 v2 k3 v3
MGET k1 k2 k3
# benchmark pipelining
redis-benchmark -t set -n 100000 -P 16 # -P = pipeline size
# don't pipeline too many at once (memory for queued replies)
# batch in groups of 100-1000Memory Optimization
Memory is Redis's primary constraint. Use the right structure: hashes for objects (listpack encoding is 5-10x smaller than separate keys). Tune encoding thresholds to keep structures in compact form. 'Key bucketing' (grouping small keys into one hash) dramatically reduces per-key overhead. Set maxmemory + an eviction policy to prevent OOM. Monitor used_memory and fragmentation ratio.
# use the right data structure (memory vs speed trade-offs)
# store small objects as hashes (listpack encoding is compact)
HSET user:1 name "Alice" age 30 # better than 3 string keys
# tune encoding thresholds (redis.conf)
hash-max-listpack-entries 128 # hash -> hashtable above this
hash-max-listpack-value 64 # field value size limit
list-max-listpack-size -2 # listpack max size
set-max-intset-entries 512 # intset -> hashtable above this
zset-max-listpack-entries 128
# use hashes for many small keys (key bucketing)
# instead of SET user:1:clicks 5; SET user:2:clicks 3
# use HSET clicks user:1 5 user:2 3
# monitor memory
INFO memory | grep used_memory_human
MEMORY STATS
# set maxmemory and an eviction policy
maxmemory 4gb
maxmemory-policy allkeys-lruEviction Policies
Eviction policy controls behavior when memory is full. For a pure cache: allkeys-lru or allkeys-lfu (LFU better for skewed access patterns). For a database: noeviction (writes fail rather than lose data). For mixed use: volatile-lru (evict only TTL'd keys, keep persistent ones). LFU (4.0+) tracks access frequency, often outperforming LRU. Monitor evicted_keys to size maxmemory correctly.
# when maxmemory is reached, Redis evicts keys per the policy
# maxmemory-policy options:
# no eviction (writes fail with OOM error)
maxmemory-policy noeviction
# LRU (Least Recently Used) — approximate LRU
allkeys-lru # evict any key (cache use case)
volatile-lru # only evict keys with TTL set
# LFU (Least Frequently Used) — better for skewed access
allkeys-lfu
volatile-lfu
# random eviction
allkeys-random
volatile-random
# TTL-based (evict soonest-expiring)
volatile-ttl
# for a cache: allkeys-lru or allkeys-lfu (best hit rate)
# for a database: noeviction (never lose data silently)
# mixed (some persistent, some cache): volatile-lruBenchmarking
redis-benchmark measures throughput and latency under load. -c (concurrency) simulates clients; -P (pipelining) tests batched performance. --latency shows real-time round-trip time. For accurate results, run the benchmark from a machine close to Redis (network dominates). Redis 6+ supports --threads for multi-threaded benchmarking. Typical single-node performance: 100K+ ops/sec on commodity hardware.
# redis-benchmark: built-in performance testing
redis-benchmark -t set,get -n 100000 -c 50
# -t: test commands, -n: number of requests, -c: concurrent clients
# test with pipelining
redis-benchmark -t set -n 100000 -c 50 -P 16
# test a specific command with a specific key
redis-benchmark -t get -n 100000 -r 100000 -q
# -r: random keys (INSERT random keys for testing)
# latency check (100 requests, shows distribution)
redis-cli --latency
redis-cli --latency-history # rolling update
redis-cli --latency-dist # histogram
# in-memory test (no network overhead)
redis-benchmark -t set -n 100000 -q --threads 4
# monitor while benchmarking
redis-cli INFO stats | grep instantaneous_ops_per_secAdministration
Server Info & Monitoring
INFO is the primary monitoring command — check memory, clients, persistence, and stats sections regularly. MEMORY DOCTOR and LATENCY DOCTOR provide automated analysis. SLOWLOG catches slow commands. CLIENT LIST shows all connections (watch for leaks — each consumes memory). Set up external monitoring (Prometheus + redis_exporter, Datadog) for historical graphs and alerts on memory, connections, and replication lag.
# comprehensive server information
INFO # all sections
INFO server # version, uptime, config
INFO clients # connected clients
INFO memory # memory usage, fragmentation
INFO persistence # RDB/AOF status
INFO stats # ops/sec, hit rate, keyspace
INFO replication # primary/replica state
INFO keyspace # keys per database
# real-time stats
LATENCY DOCTOR # latency analysis
MEMORY DOCTOR # memory analysis
# live monitoring (subscribe to a special channel)
SUBSCRIBE __redis__:invalidate
# slow query log
SLOWLOG GET 10
SLOWLOG LEN
# client connections
CLIENT LIST
CLIENT KILL ID <id> # disconnect a clientClient Management
CLIENT LIST identifies connections by address, name, and current command — invaluable for finding leaks or stuck clients. CLIENT SETNAME labels connections for easy identification (set this in your app on connect). CLIENT PAUSE blocks clients briefly for safe migrations/snapshots. Set a timeout to clean up idle connections. maxclients prevents connection exhaustion (each client uses ~some memory).
# list all connected clients
CLIENT LIST
# id, addr, name, age, idle, flags, db, cmd
# name your connections (for identification)
CLIENT SETNAME "my-app-worker-1"
# get the current client's info
CLIENT INFO
# get the current client's ID
CLIENT ID
# kill a client
CLIENT KILL ADDR 1.2.3.4:12345
CLIENT KILL ID 42
CLIENT KILL TYPE normal # kill all normal clients
# pause clients (for maintenance, 10 seconds)
CLIENT PAUSE 10000
# set a timeout for idle clients
CONFIG SET timeout 300 # 5 minutes
# max clients
CONFIG SET maxclients 10000Configuration
CONFIG SET changes settings at runtime (no restart needed); CONFIG REWRITE persists them to redis.conf. Always test config changes in staging. Key production settings: maxmemory + eviction policy (prevent OOM), save + appendonly (persistence), tcp-keepalive (detect dead clients), timeout (clean idle connections). Version-control your redis.conf. CONFIG GET * shows everything — useful for auditing.
# view config (runtime changes with CONFIG SET)
CONFIG GET maxmemory
CONFIG GET maxmemory-policy
CONFIG GET save
CONFIG GET appendonly
# change config at runtime
CONFIG SET maxmemory 4gb
CONFIG SET maxmemory-policy allkeys-lru
CONFIG SET slowlog-log-slower-than 10000
# persist config changes to redis.conf
CONFIG REWRITE
# view all config (large output)
CONFIG GET *
# typical redis.conf (production):
# bind 127.0.0.1
# protected-mode yes
# port 6379
# maxmemory 4gb
# maxmemory-policy allkeys-lru
# save 3600 1
# appendonly yes
# appendfsync everysec
# tcp-keepalive 300
# timeout 0Debugging & Troubleshooting
OBJECT ENCODING reveals the internal representation (debugging memory). OBJECT IDLETIME finds cold keys (candidates for eviction). MEMORY USAGE per key identifies memory hogs. DEBUG SLEEP tests how your app handles latency. LATENCY tracks events (expired, eviction, fast keys). Active defragmentation (4.0+) automatically reclaims fragmented memory online. Use DEBUG commands sparingly in production.
# check if a key exists and its type
EXISTS mykey
TYPE mykey
OBJECT ENCODING mykey
OBJECT IDLETIME mykey # seconds since last access
# memory usage per key
MEMORY USAGE mykey
# find keys blocking the server (DEBUG SLEEP for testing)
DEBUG SLEEP 2 # blocks for 2 seconds (testing only)
# check the latency
LATENCY HISTORY event
LATENCY GRAPH event
# object frequency (LFU mode)
OBJECT FREQ mykey
# inspect a key's internal structure (debugging)
DEBUG OBJECT mykey # raw internal info
# last saved RDB timestamp
LASTSAVE
# active defragmentation (4.0+)
CONFIG SET activedefrag yes
MEMORY MALLOC-STATSUpgrades & Maintenance
For zero-downtime upgrades, use replication: upgrade replicas first, failover, then upgrade the old primary. For clusters, do rolling restarts one node at a time (failover primaries first). Always back up (BGSAVE + copy RDB) before upgrading. Read release notes for breaking changes. Test in staging. Redis maintains backward compatibility across minor versions, but major versions may have migration steps.
# upgrade a standalone Redis:
# 1. save the dataset
SAVE
# 2. stop Redis
redis-cli SHUTDOWN
# 3. replace the binary
# 4. start the new version
redis-server /etc/redis/redis.conf
# upgrade a primary/replica pair (zero downtime):
# 1. upgrade the REPLICA first
SHUTDOWN
# 2. start new version on replica
# 3. promote replica to primary (SENTINEL FAILOVER)
# 4. upgrade the old primary (now a replica)
# 5. failback if desired
# upgrade a cluster:
# 1. upgrade one node at a time (rolling)
# 2. for each node: CLUSTER FAILOVER (if primary), upgrade, restart
# always back up before upgrading
# check the release notes for breaking changes
# test the upgrade in staging first
# rolling restart of a cluster (script)
for node in $nodes; do
redis-cli -h $node CLUSTER FAILOVER
redis-cli -h $node SHUTDOWN
ssh $node "systemctl start redis"
doneCommon Patterns
Caching Pattern (Cache-Aside)
Cache-aside is the most common Redis pattern: read from cache, fall back to DB on miss, populate cache. Set a TTL to bound staleness. For writes, either write-through (update cache on DB write) or invalidate (delete the cache key). Invalidation is safer (no race to repopulate), but write-through keeps the cache warm. Cache stampede protection: use a lock or 'dogpile' pattern to prevent many requests hitting the DB simultaneously on a miss.
# cache-aside: app checks cache, falls back to DB, fills cache
# pseudo-code:
function get_user(id):
# 1. check cache
data = GET("user:" + id)
if data:
return JSON.parse(data)
# 2. cache miss -> query database
data = db.query("SELECT * FROM users WHERE id = ?", id)
# 3. fill cache (with TTL to avoid stale data)
SET("user:" + id, JSON.stringify(data), "EX", 3600)
return data
# write-through: update cache when DB changes
function update_user(id, data):
db.update(id, data)
SET("user:" + id, JSON.stringify(data), "EX", 3600)
# cache invalidation:
DEL("user:" + id) # when the DB record is deletedDistributed Locks
Distributed locks with SET NX EX are simple but require care: always set a TTL (prevent deadlocks if the holder crashes), and use a unique token to release only your own lock (atomic check-and-delete via Lua). For higher safety across failure scenarios, the Redlock algorithm uses multiple independent Redis instances. Locks aren't perfectly safe (clock drift, GC pauses) — for strong correctness guarantees, consider a consensus system (etcd, ZooKeeper). Redis locks are fine for most practical coordination.
# SET NX with expiry = simple distributed lock
SET lock:resource "token-abc" NX EX 10
# returns OK if acquired, nil if already held
# release: only delete if the value matches (avoid releasing others' locks)
# (requires a Lua script for atomicity)
EVAL "
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0
" 1 lock:resource "token-abc"
# Redlock algorithm (multi-instance, higher safety):
# 1. get the time
# 2. SET NX on N (usually 5) independent Redis instances
# 3. if acquired on majority (3/5) within validity window -> locked
# 4. otherwise, release on all instances and retry
# lock options:
# EX 10 # expire after 10 seconds (safety)
# NX # only if not exists
# "token" # unique value per lock holder (for safe release)Rate Limiting
Fixed window (INCR+EXPIRE) is simplest but allows 2x burst at window boundaries. Sliding window (sorted set of timestamps) is more accurate but uses more memory. Token bucket allows bursting up to a max. All can be atomic with Lua scripts (important for correctness under concurrency). For per-user rate limiting at scale, the fixed-window approach is usually sufficient and very memory-efficient.
# fixed window rate limiter (simple)
# INCR + EXPIRE on first request
count = INCR("rate:user:1")
if count == 1:
EXPIRE("rate:user:1", 60) # 60-second window
if count > 100:
return "Rate limited"
# sliding window rate limiter (sorted set)
now = current_time_millis()
ZADD("rate:user:1", now, now) # add current request
ZREMRANGEBYSCORE("rate:user:1", 0, now - 60000) # remove old
EXPIRE("rate:user:1", 60)
count = ZCARD("rate:user:1") # current count
if count > 100:
return "Rate limited"
# token bucket (Lua script for atomicity)
# maintains a bucket of tokens refilled over time
# see: https://redis.io/docs/patterns/distributed-locks/
# GCRA (Generic Cell Rate Algorithm) — elegant single-key versionLeaderboard & Ranking
Sorted sets make leaderboards trivial: ZADD to update scores, ZREVRANGE for top N, ZREVRANK for a player's position. ZINCRBY atomically updates scores. For time-bucketed leaderboards (daily/weekly), use a key per period with a TTL. Pagination uses ZREVRANGE with offset. For percentile ranks, combine ZCARD and ZREVRANK. This is one of Redis's most natural and powerful use cases.
# sorted set = perfect for leaderboards
ZADD leaderboard 100 "alice" 250 "bob" 150 "carol"
# top 10 players (highest scores)
ZREVRANGE leaderboard 0 9 WITHSCORES
# a player's rank
ZREVRANK leaderboard "alice" # 0-indexed from top
# update a score (atomic)
ZINCRBY leaderboard 50 "alice"
# players within a score range (e.g., silver tier 100-199)
ZRANGEBYSCORE leaderboard 100 199
# percentile rank
total = ZCARD(leaderboard)
rank = ZREVRANK(leaderboard "alice"
percentile = (total - rank) / total * 100
# pagination (page 3, 10 per page)
ZREVRANGE leaderboard 20 29 WITHSCORES
# time-bucketed leaderboards (weekly)
ZADD leaderboard:2025-W01 100 "alice"
EXPIRE leaderboard:2025-W01 604800 # 1 week TTLSession Store
Redis is the classic session store: fast, supports TTL (automatic expiry), and is shared across app servers. Use a hash per session with a sliding TTL (refresh on each request). Track a user's sessions in a set for 'logout everywhere' features. In a cluster, use hash tags ({user:1}) to keep a user's session and session-index in the same shard. Rotate session IDs on login/privilege changes for security.
# Redis is ideal for web sessions (fast, expiring, shared)
# store session as a hash with TTL
HSET session:abc123 user_id 1 last_active 1700000000 data "..."
EXPIRE session:abc123 1800 # 30-minute timeout
# update session (and refresh TTL)
HSET session:abc123 last_active 1700001000
EXPIRE session:abc123 1800 # sliding expiration
# destroy a session
DEL session:abc123
# list active sessions for a user (index by user)
SADD user:1:sessions "session:abc123"
SMEMBERS user:1:sessions # all session IDs
# cluster-friendly: use a hash tag for the user's sessions
SADD {user:1}:sessions "session:abc123"
# enables multi-key operations on the user's session data
# rotate session ID on privilege change (security)
RENAME session:old session:new
EXPIRE session:new 1800Lua Scripting
EVAL Basics
EVAL runs Lua scripts atomically — while a script runs, no other command executes, making it ideal for multi-step atomic operations. KEYS and ARGV pass data from the caller; never build keys by string concatenation in the script (violates cluster rules). redis.call raises errors (halts), redis.pcall returns them. Scripts are cached by their SHA1 hash, so repeated calls use EVALSHA for efficiency.
# run a Lua script inline
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey "hello"
# 1 = number of keys
# mykey = KEYS[1]
# "hello" = ARGV[1]
# return types: string/integer/boolean/table (nil/true/false)
EVAL "return 42" 0
EVAL "return {1, 2, 3}" 0
EVAL "return redis.call('GET', KEYS[1])" 1 mykey
# redis.call() raises error if command fails (stops script)
# redis.pcall() returns error as a table (can handle)
EVAL "local ok, err = pcall(redis.call, 'GET', 'missing'); return err" 0EVALSHA & Script Cache
EVALSHA runs a previously loaded script by its SHA1 hash, avoiding resending the script body on every call — a major bandwidth win for apps that run the same script repeatedly. On cache miss (after SCRIPT FLUSH or restart), Redis returns NOSCRIPT; clients catch this and fall back to EVAL + SCRIPT LOAD. Always use libraries (redis-py, ioredis) that handle this transparently.
# load script once, get its SHA1
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# returns: "fa00..." (the SHA1)
# execute by hash (saves bandwidth on repeated calls)
EVALSHA "fa00..." 1 mykey
# check if a script is cached
SCRIPT EXISTS "fa00..." "abcd..."
# returns: 1) 1 2) 0
# flush the script cache
SCRIPT FLUSH
# list all running scripts (NOSCRIPT errors = cache miss -> reload)
SCRIPT DEBUG SYNC # blocking debug mode
SCRIPT DEBUG NO # disableAtomic Operations
Lua scripts are the canonical way to do atomic multi-command operations in Redis (no MULTI/EXEC race). Compare-and-set, semaphore acquire, and queue migrations all benefit. Scripts block the server while running — keep them short and avoid loops over many keys (which can stall other clients). For very long work, use Redis Functions (Redis 7.0+) which are declared and versioned like stored procedures.
# atomic compare-and-set (only update if value matches)
local cur = redis.call('GET', KEYS[1])
if cur == ARGV[1] then
return redis.call('SET', KEYS[1], ARGV[2])
end
return 0
# atomic 'decrement if positive' (semaphore / rate limit)
local val = tonumber(redis.call('GET', KEYS[1]) or 0)
if val > 0 then
redis.call('DECR', KEYS[1])
return 1
end
return 0
# atomic list pop + push (move between queues)
local item = redis.call('RPOP', KEYS[1])
if item then
redis.call('LPUSH', KEYS[2], item)
end
return itemRedis Functions (7.0+)
Redis Functions (7.0+) are the modern replacement for ad-hoc EVAL scripts: they're registered, named, and versioned like database stored procedures. A library bundles multiple functions; FCALL invokes them. Functions are persisted to RDB/AOF (survive restarts), unlike ephemeral EVAL caches. They make scripting more maintainable for non-trivial logic, and are easier to audit and version control.
# register a library of functions
FUNCTION LOAD "#!lua name=mylib
redis.register_function('myset', function(keys, args)
return redis.call('SET', keys[1], args[1])
end)
redis.register_function('double', function(keys, args)
local v = tonumber(redis.call('GET', keys[1]))
return redis.call('SET', keys[1], v * 2)
end)
"
# call a function
FCALL myset 1 mykey "hello"
FCALL double 1 counter
# list libraries
FUNCTION LIST
FUNCTION LIST WITHCODE
# delete a library
FUNCTION DELETE mylib
# dump and restore (for backup / migration)
FUNCTION DUMP
FUNCTION RESTORE <payload>Debugging Scripts
Use redis.log() to emit messages to the Redis log for debugging without affecting return values. redis-cli --ldb is an interactive Lua debugger: set breakpoints, step through code, inspect locals. SCRIPT DEBUG SYNC makes the server block on the script so the debugger can step. Always test scripts in staging — a runaway loop can lock the entire Redis server. Keep scripts deterministic (no random/time) for replication correctness.
# LOG writes to the Redis log (visible in redis-cli)
EVAL "redis.log(redis.LOG_NOTICE, 'hello from lua'); return 1" 0
# levels: LOG_DEBUG, LOG_VERBOSE, LOG_NOTICE, LOG_WARNING
# return the error object on pcall failure
EVAL "local ok, err = pcall(function()
error('something broke')
end)
return {ok=tostring(ok), err=tostring(err)}" 0
# debugger (Redis 5.0+)
SCRIPT DEBUG SYNC # synchronous, blocking
TDEBUG # step / continue / print
# use redis-cli --ldb to launch debugger:
# redis-cli --ldb --eval myscript.lua key1 key2 , arg1 arg2
# print variables in the debugger
print myvar
step
continueMonitoring & Troubleshooting
INFO & LATENCY
INFO is the single most important monitoring command — it reports memory, clients, replication, and throughput in one shot. Watch mem_fragmentation_ratio (ideal ~1.0–1.5); >1.5 wastes memory, <1 means Redis is swapping (catastrophic for latency). LATENCY DOCTOR analyzes recent slow events and suggests fixes. Monitor keyspace_misses vs keyspace_hits to track your cache hit ratio.
# INFO: server, clients, memory, stats, replication, cpu
INFO # everything
INFO memory # memory section only
INFO replication # replica lag, master link
INFO stats # ops/sec, keyspace hits/misses
# key metrics to watch:
# used_memory_rss # RSS from OS (real footprint)
# mem_fragmentation_ratio # >1.5 = fragmentation, <1 = swapping (bad!)
# connected_clients # client connections
# instantaneous_ops_per_sec
# keyspace_misses # cache hit ratio = hits / (hits + misses)
# LATENCY: samples slow events
LATENCY HISTORY event-name
LATENCY DOCTOR # human-readable diagnosis
LATENCY RESET
# LATENCY GRAPH event-name (ASCII art)SLOWLOG & CLIENT LIST
SLOWLOG captures commands slower than the configured threshold (default 10ms) — invaluable for finding the queries causing latency spikes. KEYSPACE operations like KEYS * and SMEMBERS on huge sets are common culprits. CLIENT LIST shows every connection with idle time and current command; use CLIENT SETNAME to label your app's connections so you can identify which pool a stuck client belongs to.
# SLOWLOG: record slow commands (over slowlog-log-slower-than, default 10ms)
CONFIG SET slowlog-log-slower-than 10000 # 10ms in microseconds
CONFIG SET slowlog-max-len 128 # keep last 128 entries
SLOWLOG GET # last 10 slow commands
SLOWLOG GET 5 # last 5
SLOWLOG RESET # clear
# each entry: id, timestamp, duration (us), command args, client, addr
# CLIENT LIST: see all connected clients
CLIENT LIST
# fields: id, addr, fd, name, age, idle, flags, db, cmd, ...
# kill a stuck client
CLIENT KILL ADDR 10.0.0.1:12345
CLIENT KILL ID 42
# name your connections for easier debugging
CLIENT SETNAME my-app-workerMEMORY & OBJECT
MEMORY USAGE reports the precise bytes for one key (with SAMPLES 0 for full accuracy on large collections). OBJECT ENCODING reveals Redis's internal format — small structures use compact encodings (listpack, ziplist) that are CPU- and memory-efficient, automatically upgrading to hash tables / skiplists as they grow. Knowing the encoding helps explain why a key is bigger or slower than expected.
# MEMORY USAGE: bytes used by a single key
MEMORY USAGE key
MEMORY USAGE bigset SAMPLES 0 # scan all elements (accurate)
# MEMORY STATS: detailed allocator stats
MEMORY STATS
# MEMORY DOCTOR: diagnosis
MEMORY DOCTOR
# OBJECT ENCODING: see internal representation
OBJECT ENCODING mylist # quicklist / ziplist / listpack / hashtable
OBJECT REFCOUNT mykey
OBJECT IDLETIME mykey # seconds since last access
OBJECT FREQ mykey # LFU access frequency (only with maxmemory-policy LFU)
# why is my key so big? use DEBUG OBJECT (careful, can be slow)
DEBUG OBJECT mykeyBigkey & Keyspace Scan
Never use KEYS in production — it blocks the server. SCAN is the cursor-based, non-blocking alternative; iterate until the cursor returns to 0. --bigkeys and --memkeys are CLI helpers built on SCAN that surface the largest keys without blocking. Big keys (especially collections with millions of elements) cause latency spikes during expiry/eviction — find and split them (e.g. shard a huge sorted set by time).
# find big keys safely (production-safe, samples)
redis-cli --bigkeys
# scans keyspace, reports largest of each type
# more accurate: --memkeys (memory usage per key)
redis-cli --memkeys
redis-cli --hotkeys # with LFU policy, finds frequently-accessed keys
# SCAN: iterate keys without blocking (never use KEYS *)
SCAN 0 MATCH user:* COUNT 100
# returns: next-cursor, [key1, key2, ...]
SCAN <next-cursor> ...
# cursor 0 = done
# scan a hash field-by-field
HSCAN myhash 0 COUNT 100
SSCAN myset 0 MATCH prefix*
ZSCAN myzset 0
# inspect a key's TTL (left to live)
TTL mykey # seconds (-1 = no expire, -2 = no key)
PTTL mykey # millisecondsCluster Health & Failover
CLUSTER NODES and CLUSTER INFO are the primary cluster health checks — look for cluster_state:ok and that all slots are assigned (16384 total). CLUSTER FAILOVER on a replica triggers a controlled master switch; FORCE skips the initial sync (faster but slight data loss risk) and TAKEOVER bypasses cluster consensus (use only in emergencies). Always run --cluster check after reshards or incidents to catch orphaned slots or split-brain states.
# cluster node status
CLUSTER NODES
# fields: id, addr, flags (master/slave/fail?), slots, ping/pong
# cluster-wide health
CLUSTER INFO
# cluster_state:ok / cluster_slots_assigned / cluster_slots_ok
# cluster_known_nodes
# slot ownership (which node owns a key)
CLUSTER KEYSLOT mykey # hash slot number
CLUSTER COUNTKEYSINSLOT 1234
# reshard slots (use redis-cli --cluster reshard)
redis-cli --cluster reshard 127.0.0.1:7000 \
--cluster-from <node-id> --cluster-to <node-id> \
--cluster-slots 1000 --cluster-yes
# manual failover (on a replica)
CLUSTER FAILOVER # wait for sync
CLUSTER FAILOVER FORCE # skip sync (faster, slight data loss risk)
CLUSTER FAILOVER TAKEOVER # no consensus (emergency only)
# verify cluster after changes
redis-cli --cluster check 127.0.0.1:7000
redis-cli --cluster fix 127.0.0.1:7000 # auto-repair minor issues関連する Redis スニペット
Copy-paste ready code for common tasks.
Strings & Counters
Set, get, increment, and expire string values.
Lists & Queues
Build queues and stacks with list operations.
Hashes
Store object fields and values efficiently.
Sets
Manage unique collections and compute intersections.
Sorted Sets & Leaderboards
Rank items by score for leaderboards and scheduling.
Pub/Sub
Broadcast messages to subscribed clients.
Persistence
Configure RDB snapshots and AOF append logs.
Keys & Expiration
Set TTL, scan keys, and inspect the keyspace.
Was this helpful?