Use Cases & Patterns

This content was generated with the assistance of AI and should be verified against the official Redis documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Examples on this page target Redis Open Source 8.10 (8.10.2) with the Jedis 8 and Lettuce 7.7 client libraries, and reuse the Bookshop scenario from the rest of this section — a bookshop’s cart, catalog, and storefront — rather than defining a new domain. Most patterns below are a handful of commands used in a specific shape; where a fuller treatment of the underlying data structure or command family already exists elsewhere in this section or in the application-layer pages, this page links to it instead of repeating it.

Caching: cache-aside, read-through, write-behind

Cache-aside (the default shape) has the application check the cache first, fall through to the database on a miss, then populate the cache before returning:

127.0.0.1:6379> GET bookshop:cache:book:9780000000001
(nil)
127.0.0.1:6379> SET bookshop:cache:book:9780000000001 "{\"title\":\"The Redis Way\",\"price\":19.99}" EX 3600
OK

A flat TTL makes every cached key expire in lockstep after a bulk warm-up, so many requests miss at once (a cache stampede). Jittering the TTL spreads expirations out, and a short-lived per-key lock lets only one caller recompute a value while the rest wait or serve the stale copy a little longer:

package com.example.bookshop;

import redis.clients.jedis.RedisClient;
import redis.clients.jedis.params.SetParams;

import java.time.Duration;
import java.util.concurrent.ThreadLocalRandom;

public class BookCacheService {

    private final RedisClient redis;

    public BookCacheService(RedisClient redis) {
        this.redis = redis;
    }

    public String getBook(String isbn, java.util.function.Supplier<String> loadFromDb) {
        String key = "bookshop:cache:book:" + isbn;
        String cached = redis.get(key);
        if (cached != null) {
            return cached;
        }
        String lockKey = "bookshop:lock:book:" + isbn;
        if (redis.set(lockKey, "1", SetParams.setParams().nx().px(5_000)) == null) {
            return redis.get(key); // another caller is recomputing; retry/return stale
        }
        try {
            String value = loadFromDb.get();
            int jitterSeconds = ThreadLocalRandom.current().nextInt(60);
            redis.set(key, value, SetParams.setParams().ex(Duration.ofHours(1).toSeconds() + jitterSeconds));
            return value;
        } finally {
            redis.del(lockKey);
        }
    }
}

Read-through pushes this same miss-then-populate logic behind the cache client or a caching abstraction (Spring’s @Cacheable, an ORM’s second-level cache) so application code never sees the miss path directly — see Second-Level Cache for that abstraction on the Java/Hibernate side. Write-behind instead writes to the cache synchronously and queues the database write to happen asynchronously (commonly via a stream), trading a small durability window for lower write latency; it needs a retry/dead-letter path for the deferred write, unlike the simpler write-through shape (write cache and database together, synchronously).

Prefetching populates hot keys before they’re requested — a scheduled job re-warms the top-selling titles' cache entries a few minutes before their TTL would otherwise lapse, avoiding the miss entirely for predictable hot data:

127.0.0.1:6379> EXPIRETIME bookshop:cache:book:9780000000001
(integer) 1732550400

This derives from Caching with Redis.

sequenceDiagram participant A as Client A participant B as Client B participant C as Cache participant D as Database A->>C: GET bookshop:cache:book:... C-->>A: (nil) miss B->>C: GET bookshop:cache:book:... C-->>B: (nil) miss A->>C: SET bookshop:lock:book:... NX PX 5000 C-->>A: OK (lock acquired) B->>C: SET bookshop:lock:book:... NX PX 5000 C-->>B: (nil) (lock held by A) B->>C: GET bookshop:cache:book:... (retry/serve stale) A->>D: load book D-->>A: book data A->>C: SET bookshop:cache:book:... EX 3600+jitter A->>C: DEL bookshop:lock:book:...

Session store

A session maps naturally to a Redis hash, one field per attribute, keyed per user or per session ID — bookshop:cart:<userId> for the Bookshop’s shopping cart is the same shape:

127.0.0.1:6379> HSET bookshop:cart:42 9780000000001 2 9780000000002 1
(integer) 2
127.0.0.1:6379> HEXPIRE bookshop:cart:42 1800 FIELDS 1 9780000000001
1) (integer) 1
127.0.0.1:6379> EXPIRE bookshop:cart:42 7200
(integer) 1

HEXPIRE key seconds FIELDS numfields field [field …​] expires an individual hash field independently of the key’s own TTL — useful when a session hash mixes long-lived attributes (user ID, cart total) with short-lived ones (a one-time promo code applied to the cart) that should drop out on their own schedule. This is separate from the framework-level session store already covered on this site: Spring Session’s Redis-backed HttpSession implementation will be covered on the future Spring Boot caching page, and Logout and Session Management already covers the OAuth/OIDC session-store material (the client application’s own session versus the authorization server’s) that this page doesn’t repeat.

This derives from Session management with Redis and HEXPIRE.

Rate limiting

Fixed window: an atomic increment-with-expiry counter per window. INCREX key BYINT increment UBOUND cap EX seconds ENX increments and enforces the cap in one round trip — ENX sets the TTL only when the key is first created in a window, so later increments don’t push the window’s start out, and UBOUND makes the reply’s actual-increment element 0 once the cap is reached, so the caller can check it directly instead of comparing the returned value to the limit:

127.0.0.1:6379> INCREX bookshop:ratelimit:42 BYINT 1 UBOUND 100 EX 60 ENX
1) (integer) 1
2) (integer) 1

Fixed windows allow up to double the limit across a window boundary (a burst just before and just after the reset). Sliding window avoids that by keeping one sorted-set member per request, scored by its timestamp, trimming everything older than the window on each check:

127.0.0.1:6379> ZREMRANGEBYSCORE bookshop:ratelimit:42 -inf (1732550340000
(integer) 3
127.0.0.1:6379> ZADD bookshop:ratelimit:42 1732550400000 "req-9f1c"
(integer) 1
127.0.0.1:6379> ZCARD bookshop:ratelimit:42
(integer) 47
127.0.0.1:6379> EXPIRE bookshop:ratelimit:42 60
(integer) 1

Token bucket allows short bursts up to the bucket size while enforcing a steady long-run refill rate; because it needs to read the bucket’s current level, compute the elapsed-time refill, and write the new level all atomically, it’s naturally a Lua script rather than a single command:

-- KEYS[1] = bookshop:ratelimit:42, ARGV = capacity, refill_per_sec, now_ms, cost
local capacity, refill_rate, now, cost = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3]), tonumber(ARGV[4])
local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(bucket[1]) or capacity
local ts = tonumber(bucket[2]) or now
tokens = math.min(capacity, tokens + (now - ts) / 1000 * refill_rate)
if tokens < cost then
    return 0
end
redis.call('HSET', KEYS[1], 'tokens', tokens - cost, 'ts', now)
redis.call('EXPIRE', KEYS[1], 60)
return 1
Algorithm Accuracy Burst handling Implementation cost

Fixed window

Allows up to 2x limit at window boundaries

Poor — boundary burst

One command (INCREX …​ ENX)

Sliding window (sorted set)

Exact, no boundary artifact

None beyond the configured limit

A few commands, one member per request (higher memory)

Token bucket (Lua)

Exact against the configured rate

Allows a burst up to bucket capacity, then throttles to the refill rate

One Lua script, small fixed state per key

This derives from Redis rate limiting techniques, INCREX, and EVAL/EVALSHA for the Lua script.

On the JVM, the library alternative to a hand-rolled Lua token bucket is Bucket4j, which keeps the bucket in Redis through a ProxyManager and adds bandwidths, per-key configuration and Spring Boot integration; see Bucket4j: Token Bucket and Bandwidths and Bucket4j: Distributed Rate Limiting.

Semantic cache and agent memory

A semantic cache stores an LLM prompt’s response keyed by the embedding of the prompt rather than its exact text, so a paraphrased question ("what’s the return policy for a damaged book?" vs. "can I return a book that arrived damaged?") can still hit the cache; agent memory uses the same shape to let an agent retrieve past conversation turns or facts relevant to the current context. Both are a KNN/range vector query over an index of past prompts or memories rather than a distinct feature — see Vector Search: Query Engine & Vector Sets for FT.CREATE with a VECTOR field, FT.SEARCH …​ KNN, and the distance-metric tradeoffs that apply equally here; this page adds nothing to that beyond the use case itself.

Reliable job queues

A Redis list makes a simple FIFO job queue: LPUSH enqueues, and BLMOVE source destination LEFT RIGHT timeout atomically pops the oldest job and pushes it onto a per-worker "in-progress" list in one step, so a worker that crashes mid-job doesn’t lose it — a reaper can periodically move stale entries back onto the main queue:

127.0.0.1:6379> LPUSH bookshop:jobs:emails "{\"orderId\":1042}"
(integer) 1
127.0.0.1:6379> BLMOVE bookshop:jobs:emails bookshop:jobs:emails:processing LEFT RIGHT 5
"{\"orderId\":1042}"

For anything needing multiple independent consumers, replay, or per-message acknowledgment, a stream with consumer groups is the better fit — bookshop:orders with a fulfilment group and worker-1/worker-2 consumers is exactly that shape, covered in full (including XREADGROUP, XACK, and claiming stuck messages with XAUTOCLAIM) in Streams, Pub/Sub & Keyspace Notifications; this page doesn’t repeat it.

This derives from Redis as a message queue and BLMOVE.

Distributed locks

The base primitive is a SET key token NX PX ttl — NX makes acquisition atomic (only the first caller succeeds), PX bounds how long a crashed holder can block others. Releasing must check that the caller still holds the lock (its own random token) before deleting it, so one caller can’t release a lock a different caller has since acquired after the first one’s TTL expired:

127.0.0.1:6379> SET bookshop:lock:inventory:9780000000001 "tok-a1b2" NX PX 30000
OK
127.0.0.1:6379> DELEX bookshop:lock:inventory:9780000000001 IFEQ "tok-a1b2"
(integer) 1

DELEX key IFEQ value performs that check-then-delete atomically as a single command; a Lua script comparing GET against the token before DEL is the older, equivalent way to do it on a server without DELEX. Whichever release mechanism is used, a lock alone doesn’t stop a caller that’s paused past its TTL (a long GC pause, a slow network) from acting after another caller has already acquired the lock and started — a fencing token, a number that increments on every successful acquisition and gets checked by whatever downstream resource the lock protects, closes that gap by letting the resource reject a stale caller’s write even if the lock itself has moved on.

Redlock extends the same SET NX PX primitive across N independent Redis masters, requiring a majority quorum before considering the lock held, to survive a single master’s failure without losing the lock’s mutual-exclusion guarantee — it needs a genuinely independent deployment (not just replicas of one master) to provide that guarantee, and its safety properties under clock drift and long pauses are debated, so it is not a drop-in replacement for a single-instance lock unless that multi-master independence is actually in place. ShedLock’s Redis provider (RedisLockProvider, covered in Scheduling and ShedLock) uses the single-instance SET …​ NX PX shape, which is sufficient for its purpose of skipping a duplicate @Scheduled run rather than protecting a critical section against a Byzantine failure.

This derives from Distributed locks with Redis and DELEX.

Leaderboards, dedup, unique visitors, geo, recommendations, feature store, fraud, and gateways

The remaining patterns are each a short, idiomatic use of one existing Bookshop data structure:

  • Leaderboards — a sorted set ranks by score with no separate sort step: ZADD bookshop:bestsellers 1284 "9780000000001" records a book’s unit-sold count, ZREVRANGE bookshop:bestsellers 0 9 WITHSCORES reads the top 10.

  • Dedup — a plain SET (SISMEMBER) works for an exact, bounded set; a Bloom filter trades a small false- positive rate for constant, tiny memory at scale: BF.ADD bookshop:seen-isbns 9780000000001 / BF.EXISTS bookshop:seen-isbns 9780000000001. A Cuckoo filter (CF.ADD/CF.EXISTS) adds the ability to remove an entry later, which a Bloom filter cannot do.

  • Unique visitors — a HyperLogLog counts approximate distinct values in near-constant space: PFADD bookshop:visitors:2026-09-25 "user:42" then PFCOUNT bookshop:visitors:2026-09-25.

  • Geo lookup — GEOADD bookshop:stores -3.7038 40.4168 "madrid-centro" indexes a store’s coordinates, GEOSEARCH bookshop:stores FROMLONLAT -3.70 40.42 BYRADIUS 10 km ASC finds the nearest ones.

  • Recommendation by scores — a sorted set of a user’s recently viewed titles doubles as a lightweight "more like this" signal: LPUSH bookshop:recent:42 9780000000001 capped with LTRIM bookshop:recent:42 0 49 feeds a downstream similarity step (see the vector-search page above for embedding-based recommendations).

  • Feature store — a hash per entity, one field per feature, with a TTL matching how fresh the feature needs to be: HSET bookshop:features:user:42 avg_order_value 34.20 churn_score 0.12 then EXPIRE bookshop:features:user:42 3600.

  • Fraud detection — combines several of the above: a sliding-window counter on checkout attempts per card, a Bloom filter of previously flagged device fingerprints, and a feature-store hash of the user’s recent behavior, all read in one round trip via a pipeline (see Pipelining, Transactions & Scripting) so the scoring decision stays within a tight request-time budget.

  • API gateway — the same rate-limiting keys above, checked before proxying a request, plus a short-TTL cache-aside entry per upstream response for idempotent GET routes, keep both abuse and upstream load down without a dedicated gateway product.

Two existing pages already cover the caching half of this list on their own stacks and aren’t repeated here: Second-Level Cache (Hibernate’s L2 cache, including a Redis-backed region factory) and Performance and Caching (ASP.NET Core’s IDistributedCache, which a Redis-backed implementation plugs into the same way).

This derives from Leaderboards with Redis, BF.ADD, CF.ADD, PFADD, and GEOADD.

References