Redis Use Cases: Caching, Queues, and Rate Limiting

Backend2026-10-06TryQuickToolBox

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

Cache Invalidation Strategies

Invalidation is hard. Common strategies include:

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