Strings, Hashes, and Lists
String Operations
Strings are Redis’s most versatile type — counters, locks, serialized objects, and bitmaps all use strings under the hood.
# Basic get/set
SET product:42:name "Wireless Mouse"
GET product:42:name
# Conditional set — distributed locking foundation
SETNX lock:resource "owner-1"
SET product:42:stock 100 XX # only if key exists
SET product:99:stock 50 NX # only if key does NOT exist
# Atomic numeric operations
INCR api:rate:user:1001
INCRBY page:views:daily 1
DECRBY inventory:sku:99 3
INCRBYFLOAT price:sku:99 -0.50
# Multi-key atomic operations
MSET user:1:name "Alice" user:2:name "Bob"
MGET user:1:name user:2:name
GETSET counter:last_id 42 # returns old value, sets new
String as JSON Cache
import json
redis.setex(f"product:{pid}", 600, json.dumps(product_dict))
data = json.loads(redis.get(f"product:{pid}") or "null")
Prefer hashes when individual fields update frequently.
Expiration and TTL
Every cache key should have a deliberate TTL strategy.
SET cache:homepage "<html>...</html>"
EXPIRE cache:homepage 300
TTL cache:homepage # seconds remaining; -1 = no expiry; -2 = key missing
SETEX cache:api:users 60 "[...]" # SET + EXPIRE atomically
PERSIST cache:homepage # remove expiration
# Check expiry type
TTL session:abc # seconds
PTTL session:abc # milliseconds
Use SETEX or SET ... EX n instead of separate SET + EXPIRE to avoid race conditions where a key exists without TTL.
Hash Field Operations
Hashes store field-value pairs — ideal for objects and sessions.
HSET user:1001 name "Alice" country "US" login_count 0
HGET user:1001 name
HEXISTS user:1001 phone
HDEL user:1001 country
HKEYS user:1001
HVALS user:1001
HLEN user:1001
HINCRBY user:1001 login_count 1
HSETNX user:1001 verified 1
Partial Updates Without Read-Modify-Write
# Update single field — no need to fetch entire object
redis.hset(f"user:{user_id}", "last_seen", int(time.time()))
redis.hincrby(f"user:{user_id}", "page_views", 1)
Hash vs String Memory
For objects with fewer than ~100 fields and small values, hashes use listpack encoding and consume less memory than equivalent JSON strings.
List as Queue
Lists implement FIFO queues with O(1) push/pop at ends.
# Producer
LPUSH job:queue "job-001" "job-002"
# Consumer — non-blocking
RPOP job:queue
# Consumer — blocking (efficient polling)
BRPOP job:queue 30 # wait up to 30 seconds
# Reliable queue — move to processing list atomically
RPOPLPUSH job:queue job:processing
# After successful processing:
LREM job:processing 1 "job-001"
Reliable Queue Pattern
Producer → LPUSH queue → Worker BRPOP/RPOPLPUSH → processing list → ACK (LREM)
If a worker crashes, jobs remain in job:processing for recovery scans.
List as Stack
LPUSH undo:stack "action1"
LPUSH undo:stack "action2"
LPOP undo:stack # returns "action2" (LIFO)
List as Recent Activity Feed
LPUSH user:1001:activity "viewed product:42"
LTRIM user:1001:activity 0 49 # keep only 50 items
LRANGE user:1001:activity 0 9 # latest 10
Trim and Inspect
LTRIM recent:searches 0 49
LLEN recent:searches
LINDEX recent:searches 0
LINSERT recent:searches BEFORE "query-a" "query-new"
Pipelining
Batch commands to reduce round-trip latency. Each command still executes atomically on its own — pipelining is not a transaction.
pipe = redis.pipeline()
pipe.set("key1", "val1")
pipe.incr("counter")
pipe.hset("user:1", "name", "Alice")
pipe.expire("key1", 300)
results = pipe.execute()
# redis-cli pipelining
echo -e "SET k1 v1\nINCR c1\nGET k1" | redis-cli --pipe
Pipelining 1,000 commands can reduce total latency from seconds to milliseconds compared to individual round trips.
Transactions (MULTI/EXEC)
MULTI
INCR account:1001:balance
DECR account:1002:balance
EXEC
Transactions queue commands — they do not roll back on failure mid-EXEC. Use Lua scripts for conditional logic.
Best Practices
- Always set TTL on cache keys at creation
- Use BRPOP instead of polling with sleep loops
- Keep list lengths bounded with LTRIM for activity feeds
- Use pipelining for bulk inserts (thousands of keys)
- Prefer hashes for session data with multiple fields
Common Mistakes
| Mistake | Impact |
|---|---|
| SET then EXPIRE separately | Window where key has no TTL |
| Unbounded list growth | Memory exhaustion |
| Polling RPOP in a tight loop | Wastes CPU and connections |
| Storing multi-MB strings | Blocks event loop during read/write |
| Using lists for membership tests | O(N) instead of O(1) with sets |
Troubleshooting
Blocking clients pile up:
INFO clients
# High blocked_clients from BRPOP — normal for workers; ensure enough consumers
CLIENT LIST
Queue jobs stuck in processing:
LLEN job:processing
LRANGE job:processing 0 -1
# Requeue stale jobs with a scheduled recovery script
TTL returns -1 unexpectedly:
TTL mykey
# Key exists without expiry — add EXPIRE or investigate creation path
Performance Tips
MGET/HMGETfor batch reads instead of loops- Cap list sizes with
LTRIMafter everyLPUSHin hot paths - Use connection pooling — one pool per application process
- Monitor
slowlogfor unexpectedly slowLRANGEon large lists
Production Scenario
An email delivery service processed 200K jobs/hour using BRPOP workers with RPOPLPUSH to a processing list. Crash recovery scanned job:processing every 5 minutes, requeuing jobs older than 10 minutes. Pipelined batch enqueues (LPUSH × 100) cut enqueue latency from 800ms to 12ms during traffic spikes.
Strings, hashes, and lists cover the majority of Redis application patterns — master these before moving to sets, streams, and advanced caching strategies.