At 11:47 PM on a Tuesday, an e-commerce platform serving 2.3 million daily active users hit a wall. Their PostgreSQL primary was at 98% CPU. Every product page load triggered 14 separate database queries — catalog lookups, inventory checks, session reads, and recommendation fetches — all hitting the same server. The on-call engineer had one hour before the EU market opened and traffic doubled. The fix was already in their stack, unused: a Redis 7 cluster deployed six months earlier for session storage and never extended beyond that single use case.
By 2 AM, four of those 14 queries were routed to Redis with a cache-aside pattern and a 300-second TTL. Database CPU dropped to 41%. By the following weekend, they had added Redis-backed rate limiting on their promotions API, a BullMQ job queue for order processing emails, and pub/sub fan-out for real-time inventory updates. The same Redis instance that was handling sessions at 2% utilization was now carrying a third of their application's data workload.
Redis is not a single-purpose tool. This guide covers the five production use cases that deliver the highest leverage — caching, rate limiting, pub/sub and streams, job queues, and session storage — with real Redis commands, eviction and persistence trade-offs, and the gotchas that cause production incidents.
- Redis covers five high-leverage use cases from a single deployment: caching, rate limiting, pub/sub messaging, job queues, and session storage.
- Cache-aside with
SET EXreduces database read load by 60–90% for read-heavy workloads; LRU eviction is the right default for caches, LFU for recommendation and frequency-ranked data. - Rate limiting with
INCR+EXPIREworks for fixed windows; sliding-window rate limiting using sorted sets handles burst traffic without the boundary-reset problem. - Redis Streams (
XADD/XREADGROUP) are a durable alternative to basic pub/sub for use cases that require message persistence, consumer groups, or at-least-once delivery. LPUSH/BRPOPlists work for simple queues; BullMQ and Sidekiq build on Redis Streams and sorted sets to add retries, delays, and priority lanes.- Persistence trade-offs matter per use case: caches can run RDB-only or no persistence; rate limiting and session storage need AOF for accuracy after restarts.
Background
Redis is an in-memory data structure server. That single sentence explains both its performance characteristics and its design constraints. Because all data lives in RAM, reads and writes are measured in microseconds — typically 0.1ms to 1ms for single-key operations at any reasonable QPS. Because RAM is finite and expensive, Redis is optimized for working sets, not full datasets.
The key insight is that Redis is not a replacement for a relational database. It is a complement: fast, ephemeral (or semi-durable), and purpose-built for the operations that relational databases do slowly — random key lookups, TTL-based expiry, atomic counters, and fan-out messaging.
Redis Data Structures
Understanding which data structure to use for each use case is the foundation of effective Redis usage. Using the wrong structure is the most common mistake in Redis implementations.
Strings
The simplest structure: a single key maps to a single byte sequence up to 512 MB. Used for counters (INCR), cached serialized objects, and simple flags. Operations: SET, GET, INCR, INCRBY, SETNX, SET EX.
Hashes
A map of field-value pairs under a single key. The natural fit for structured objects like user sessions or cached database rows. More memory-efficient than storing each field as a separate string key when the hash has fewer than 128 fields. Operations: HSET, HGET, HGETALL, HDEL, HEXPIRE (Redis 7.4+).
Lists
A doubly-linked list of strings. Used for queues (LPUSH/BRPOP), activity feeds, and recent-item buffers. Push to head or tail in O(1). Operations: LPUSH, RPUSH, LPOP, RPOP, BRPOP, LRANGE.
Sets
An unordered collection of unique strings. Used for unique visitor counting, tag indexing, and set operations (union, intersection, difference). Operations: SADD, SMEMBERS, SINTER, SUNION, SISMEMBER.
Sorted Sets (ZSets)
A set where every member has a floating-point score. Members are kept sorted by score. Used for leaderboards, sliding-window rate limiting, delayed job queues, and priority queues. Operations: ZADD, ZRANGEBYSCORE, ZREMRANGEBYSCORE, ZCARD.
Streams
A persistent, append-only log with consumer groups. Used as a durable message queue or event bus. Each entry has an auto-generated ID and arbitrary field-value pairs. Operations: XADD, XREAD, XREADGROUP, XACK, XLEN.
Bitmaps and HyperLogLog
Bitmaps (SETBIT / GETBIT) store per-user boolean flags in extremely compact form — 1 bit per user ID means 100 million users fit in 12.5 MB. HyperLogLog (PFADD / PFCOUNT) estimates the cardinality of a set with ~0.81% error using only 12 KB of memory — ideal for unique visitor counts at scale.
Use Case 1: Caching
Caching is the most common Redis use case and the one with the highest immediate return. The pattern is cache-aside (also called lazy loading): check Redis first, fall back to the database on a miss, write the result to Redis with a TTL, and return it.
Basic Cache-Aside with SET EX
# Write a cached product object (TTL = 300 seconds)
SET product:42 '{"id":42,"name":"Widget Pro","price":29.99,"stock":143}' EX 300
# Read — returns the JSON string or nil on miss
GET product:42
# Atomic set-if-not-exists (prevents cache stampede in single-instance setups)
SET product:42 '{"id":42,...}' EX 300 NX
# Check remaining TTL
TTL product:42
# 287When a popular cache key expires, dozens of concurrent requests can all miss and simultaneously query the database. On high-traffic endpoints, this causes a thundering herd that can saturate the database within seconds. Mitigate with probabilistic early expiration (recompute the key before it expires based on a random check), a mutex lock using SET NX EX, or a background refresh pattern where expiry triggers an async job rather than a synchronous fallback.
Eviction Policies
When Redis reaches its maxmemory limit, it must evict keys to make room. Choosing the wrong policy is a silent correctness bug for non-cache use cases.
| Policy | Behavior | Best for |
|---|---|---|
allkeys-lru |
Evict least recently used keys across all keys | General-purpose cache (most common) |
allkeys-lfu |
Evict least frequently used keys across all keys | Recommendation caches, frequency-ranked data |
volatile-lru |
LRU eviction only among keys with a TTL set | Mixed cache + persistent data in same instance |
volatile-ttl |
Evict keys with the shortest remaining TTL first | When you want the soonest-to-expire data gone first |
noeviction |
Return an error when memory is full | Session storage, rate limiting — never evict silently |
# Set eviction policy and memory limit in redis.conf
# maxmemory 4gb
# maxmemory-policy allkeys-lru
# Or at runtime (takes effect immediately, not persisted across restarts)
CONFIG SET maxmemory 4gb
CONFIG SET maxmemory-policy allkeys-lru
# Verify
CONFIG GET maxmemory-policyKeyspace Notifications
Redis can emit pub/sub messages when keys expire or are modified. This enables cache invalidation patterns where application code subscribes to expiry events and proactively refreshes the cache before the next request misses.
# Enable keyspace notifications for expired events (in redis.conf or at runtime)
CONFIG SET notify-keyspace-events Ex
# Subscribe to expiry notifications for all keys in database 0
# (run in a separate redis-cli session)
SUBSCRIBE __keyevent@0__:expiredPure caches do not need persistence. If all cache data can be rebuilt from the source of truth, disable both RDB and AOF to reduce disk I/O and eliminate the latency spike from periodic RDB saves. Set save "" in redis.conf and appendonly no. A cache that restarts warm-up time is acceptable; the database handles the cold-start misses.
Use Case 2: Rate Limiting
Redis is the standard backend for API rate limiting because atomic increment and TTL expiry happen in a single round-trip without application-level locking.
Fixed Window with INCR + EXPIRE
# Rate limit key: user 1001, current minute window
# Key format: ratelimit:{user_id}:{window}
# On each request:
INCR ratelimit:1001:2024021422
# Returns the new count (e.g., 47)
# Set TTL on first increment only (EXPIRE is a no-op if key already has a TTL)
EXPIRE ratelimit:1001:2024021422 60
# Check limit before allowing request:
# If count > 100, reject with 429 Too Many RequestsA fixed-window rate limiter allows a burst of 2x the limit across a window boundary. If your limit is 100 requests per minute and a client sends 100 requests at 11:59:59 and 100 more at 12:00:01, both windows allow the full limit — yielding 200 requests in 2 seconds. For APIs where burst traffic is genuinely harmful (payment endpoints, auth), use sliding-window rate limiting instead.
Sliding Window with Sorted Sets
# Sliding window rate limiter using a sorted set
# Score = timestamp in milliseconds, member = unique request ID (UUID or timestamp+random)
# Current time in milliseconds
SET current_time_ms 1708617600000 # (computed in application)
# Add the current request to the sorted set
ZADD ratelimit:sliding:1001 1708617600000 "req_a1b2c3d4"
# Remove all requests outside the 60-second window
ZREMRANGEBYSCORE ratelimit:sliding:1001 0 1708617540000
# (current_time_ms - 60000)
# Count remaining requests in the window
ZCARD ratelimit:sliding:1001
# Returns 47 — if >= 100, reject request
# Set TTL to auto-expire the key when idle
EXPIRE ratelimit:sliding:1001 120The sliding-window pattern above requires three commands. In a distributed setup, a race between ZADD and ZCARD can produce incorrect counts. Wrap the three commands in a Lua script executed with EVAL to guarantee atomicity — all three commands run as a single atomic unit without any other client interleaving.
GCRA / Redis Cell Module
For production-grade rate limiting, the redis-cell module implements GCRA (Generic Cell Rate Algorithm) in a single CL.THROTTLE command. It handles burst capacity, per-request cost, and returns remaining quota and retry-after headers in one atomic operation. If your Redis deployment supports modules (self-hosted or Redis Stack), this is the preferred approach over manual sorted-set implementations.
Use Case 3: Pub/Sub and Streams
Redis pub/sub enables fan-out messaging where a single publisher sends a message and all active subscribers receive it. The canonical use case is broadcasting real-time events — inventory updates, live score feeds, chat messages — across multiple application instances without polling.
Basic SUBSCRIBE / PUBLISH
# Subscriber (run in one terminal / connection)
SUBSCRIBE inventory:updates
# Publisher (run in another connection)
PUBLISH inventory:updates '{"product_id":42,"new_stock":118,"timestamp":1708617600}'
# Pattern subscribe — receive messages on any matching channel
PSUBSCRIBE inventory:*Redis pub/sub is fire-and-forget. If a subscriber is offline when a message is published, the message is permanently lost. There is no replay, no queue, and no acknowledgment. For any use case where message delivery must be guaranteed — order events, payment webhooks, audit logs — do not use basic pub/sub. Use Redis Streams instead.
Redis Streams as Durable Pub/Sub
Redis Streams are an append-only log with consumer groups. Each message is persisted until explicitly deleted. Consumers can read from any point in the stream, and consumer groups enable competing-consumer fan-out where each message is delivered to exactly one consumer in a group.
# Producer: append a message to the stream
# * means auto-generate the ID (timestamp-sequence)
XADD order:events * order_id 90123 status "placed" user_id 1001 total 149.99
# Returns: "1708617600123-0" (stream entry ID)
# Consumer: read new messages (blocking, 5-second timeout)
XREAD COUNT 10 BLOCK 5000 STREAMS order:events $
# Create a consumer group starting from the beginning of the stream
XGROUP CREATE order:events email-workers $ MKSTREAM
# Read as a consumer group (delivers each message to only one worker)
XREADGROUP GROUP email-workers worker-1 COUNT 5 BLOCK 5000 STREAMS order:events >
# Acknowledge a processed message (removes it from the pending-entries list)
XACK order:events email-workers 1708617600123-0
# Check stream length
XLEN order:events
# Trim stream to keep only the last 10,000 entries
XTRIM order:events MAXLEN ~ 10000Use Case 4: Queues and Job Processing
Redis lists provide a simple, reliable queue primitive. LPUSH adds jobs to the head; BRPOP blocks workers waiting for jobs from the tail — giving FIFO order, zero polling overhead, and sub-millisecond delivery latency.
Simple LPUSH / BRPOP Queue
# Producer: enqueue a job
LPUSH jobs:email-notifications '{"type":"order_confirmation","order_id":90123,"user_email":"alice@example.com"}'
# Worker: block for up to 30 seconds waiting for a job
# Returns: ["jobs:email-notifications", "{...}"] or nil on timeout
BRPOP jobs:email-notifications 30
# Check queue depth
LLEN jobs:email-notifications
# Peek at the next job without consuming it
LINDEX jobs:email-notifications -1Delayed Jobs with Sorted Sets
For jobs that should run at a future time — scheduled emails, retry backoff — store them in a sorted set with the execution timestamp as the score. A polling worker checks for jobs whose score is in the past.
# Schedule a job to run at Unix timestamp 1708621200 (1 hour from now)
ZADD jobs:delayed 1708621200 '{"type":"subscription_reminder","user_id":2042}'
# Worker: atomically fetch and remove all jobs due now
# (use ZRANGEBYSCORE + ZREM in a Lua script for atomicity)
ZRANGEBYSCORE jobs:delayed 0 1708617600 # jobs with score <= now
# Remove fetched jobs
ZREMRANGEBYSCORE jobs:delayed 0 1708617600For production job queues with retries, dead-letter queues, priorities, rate limiting, and observability, use BullMQ (Node.js) or Sidekiq (Ruby) rather than implementing the patterns above from scratch. Both are built on Redis and implement exactly the sorted-set + stream patterns shown here, but with battle-tested error handling, UI dashboards, and active community support. BullMQ uses Redis Streams internally; Sidekiq uses Redis lists with a sorted set for scheduled and retry jobs.
When a worker calls BRPOP and dequeues a job, the job is immediately removed from Redis. If the worker crashes before completing the job, the job is permanently lost. For reliable processing, use the RPOPLPUSH pattern (or LMOVE in Redis 6.2+) to atomically move the job to a "processing" list. On crash recovery, inspect the processing list and re-enqueue unacknowledged jobs. BullMQ and Sidekiq handle this automatically.
Use Case 5: Session Storage
Session data needs to survive across multiple application servers, expire automatically, and be readable in a single key lookup. Redis hashes with TTLs are the textbook fit: structured, fast, and self-cleaning.
Session Key Convention and HSET with TTL
# Key naming convention: session:{session_token}
# Use a cryptographically random session token as the key suffix
# Create a session (set multiple fields atomically)
HSET session:abc123xyz user_id 1001 role "admin" cart_id 7891 created_at 1708617600
# Set TTL: session expires after 30 minutes of inactivity
EXPIRE session:abc123xyz 1800
# Read a single session field
HGET session:abc123xyz user_id
# "1001"
# Read all session fields at once
HGETALL session:abc123xyz
# 1) "user_id"
# 2) "1001"
# 3) "role"
# 4) "admin"
# 5) "cart_id"
# 6) "7891"
# 7) "created_at"
# 8) "1708617600"
# Extend the TTL on activity (sliding expiry)
EXPIRE session:abc123xyz 1800
# Invalidate session on logout
DEL session:abc123xyzIf Redis also serves as a cache with an LRU eviction policy, a memory pressure spike can silently evict active session keys, logging users out without warning. For any Redis instance that holds session data, set maxmemory-policy noeviction. When memory is full, Redis will return an error on writes rather than silently deleting session data. Better to get an alertable error than a silent correctness failure.
Persistence for Session Storage
Sessions cannot be safely lost on restart — users would be unexpectedly logged out. Enable AOF (Append Only File) persistence for session-storage Redis instances:
# In redis.conf
# appendonly yes
# appendfsync everysec # fsync every second — good balance of durability and performance
# auto-aof-rewrite-percentage 100
# auto-aof-rewrite-min-size 64mb
# Verify AOF is active
CONFIG GET appendonly
# 1) "appendonly"
# 2) "yes"
# Check AOF rewrite status
INFO persistence
# aof_enabled:1
# aof_rewrite_in_progress:0
# aof_last_rewrite_time_sec:3RDB vs AOF: Choosing Per Use Case
| Use Case | Persistence Recommendation | Reasoning |
|---|---|---|
| Cache | None (save "", appendonly no) |
Cache can be rebuilt from source of truth; persistence adds unnecessary I/O |
| Rate limiting | AOF (appendfsync everysec) |
Counter loss on restart means limits reset, allowing a burst attack |
| Sessions | AOF (appendfsync everysec) |
Session loss on restart logs out all active users |
| Job queues (lists) | AOF (appendfsync everysec) |
Queued jobs are lost on restart; AOF ensures recovery |
| Pub/sub | None | Pub/sub has no persistence semantics; use Streams if persistence is required |
| Streams | AOF or RDB+AOF | Streams are the durable alternative to pub/sub; persistence is the point |
Running caches, sessions, and job queues on a single Redis instance creates conflicting requirements. Caches want LRU eviction; sessions need noeviction. Caches want no persistence; sessions need AOF. At modest scale, separate Redis instances (or separate databases with SELECT) cleanly isolate these policies. At higher scale, Redis Cluster with different cluster configurations per use case is the standard approach.
- Use
SET EXwith cache-aside for read-heavy workloads; setallkeys-lrueviction and disable persistence on cache-only instances to minimize overhead. - Fixed-window rate limiting (
INCR+EXPIRE) is simple but allows boundary bursts; use sorted-set sliding-window or the redis-cell module for strict per-client enforcement. - Basic pub/sub (
SUBSCRIBE/PUBLISH) is zero-persistence; switch to Redis Streams with consumer groups (XADD/XREADGROUP/XACK) whenever at-least-once delivery matters. - Simple queues work with
LPUSH/BRPOP; for retries, delays, and dead-letter queues, use BullMQ (Node.js) or Sidekiq (Ruby) rather than building the patterns manually. - Store sessions as hashes with
HSET+EXPIRE; always setmaxmemory-policy noevictionon session-storage instances to prevent silent session loss under memory pressure. - Align AOF vs no-persistence decisions to each use case: caches need nothing, rate limiting and sessions need AOF, and Streams should persist by design.
Working with JusDB on Redis
JusDB manages Redis deployments for engineering teams who need production-grade reliability across all five use cases — caching, rate limiting, pub/sub, queues, and session storage — without building and maintaining the operational layer themselves. Our DBAs handle cluster configuration, eviction policy tuning, AOF and RDB persistence setup, memory sizing, replication topology, keyspace notifications, and 24/7 incident response.
We also regularly audit existing Redis deployments where teams have grown beyond a single-use-case setup and hit the policy conflicts described above: a cache instance that is silently evicting session keys under load, a job queue that is losing work on restarts because persistence was never configured, or a pub/sub implementation that should have been Streams six months ago.
Explore JusDB Managed Database Services → | Talk to a DBA
Related reading: