Redis Use Cases: Caching, Queues, and Rate Limiting
You've built a web app that works fine with a few users, but as traffic grows, you notice slow page loads, duplicated background jobs, and APIs being hammered by abusive clients. These are classic scaling problems that Redis can solve elegantly. Redis is an in-memory data store that excels at three core tasks: caching, queues, and rate limiting. In this article, we'll explore each use case with practical examples and best practices.
Why Redis for These Use Cases?
Redis stores data in memory, making it incredibly fast—often sub-millisecond response times. It supports rich data structures like strings, lists, sets, sorted sets, and hashes, which map naturally to caching, queues, and rate limiting. It also offers atomic operations, pub/sub, and persistence options, making it a versatile tool for backend developers.
1. Caching with Redis
Caching is the most common Redis use case. By storing frequently accessed data in Redis, you reduce database load and speed up responses.
How Caching Works
When a request comes in, check if the data exists in Redis. If yes (cache hit), return it directly. If not (cache miss), fetch from the database, store it in Redis with an expiration time, and return it. This pattern is called cache-aside.
// Node.js example using ioredis
const Redis = require('ioredis');
const redis = new Redis();
async function getUser(userId) {
const cacheKey = `user:${userId}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600); // 1 hour TTL
return user;
}
Best Practices for Caching
- Set a TTL: Always set an expiration time to avoid stale data and memory bloat.
- Use appropriate data structures: For simple key-value, use strings. For objects, use hashes to update fields individually.
- Handle cache invalidation: Update or delete cache entries when the underlying data changes.
- Monitor hit rate: A low hit rate means your cache isn't effective; adjust keys or TTLs.
Cache Invalidation Strategies
Invalidation is hard. Common strategies include:
- Time-based: Rely on TTL. Simple but may serve stale data.
- Write-through: Update cache whenever you update the database.
- Write-behind: Update cache first, then asynchronously update the database (risk of data loss).
2. Queues with Redis
Queues decouple time-consuming tasks from the request-response cycle. For example, sending emails, processing images, or generating reports can be done asynchronously.
Implementing a Simple Queue
Redis lists are perfect for queues. Use LPUSH to add jobs to the left and BRPOP to block and pop from the right (FIFO).
// Producer: add job
await redis.lpush('email_queue', JSON.stringify({ to: 'user@example.com', subject: 'Welcome' }));
// Consumer: process job (in a worker)
while (true) {
const job = await redis.brpop('email_queue', 0); // 0 = block indefinitely
const { to, subject } = JSON.parse(job[1]);
await sendEmail(to, subject);
}
Reliable Queues with BRPOPLPUSH
To avoid losing jobs if a worker crashes, use BRPOPLPUSH to atomically move the job to a processing list. After successful processing, remove it from the processing list. If the worker dies, another worker can recover jobs from the processing list.
const job = await redis.brpoplpush('email_queue', 'processing_queue', 0);
try {
// process job
await redis.lrem('processing_queue', 1, job);
} catch (err) {
// handle error, maybe requeue
}
Dedicated Queue Libraries
For production, consider libraries like Bull (Node.js), RQ (Python), or Sidekiq (Ruby). They provide retries, scheduling, and monitoring out of the box.
3. Rate Limiting with Redis
Rate limiting protects your API from abuse and ensures fair usage. Redis's atomic operations make it ideal for counting requests.
Fixed Window Rate Limiting
Increment a counter for each user per time window. If the count exceeds the limit, reject the request.
async function isAllowed(userId, limit = 100, windowSec = 60) {
const key = `rate:${userId}:${Math.floor(Date.now() / 1000 / windowSec)}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, windowSec);
return count