Redis Explained: Why Companies Use It for Caching

Your Database Was Not Built for This

Your application launches. Traffic is light, every query hits Postgres, response times are fine. Then you get featured somewhere. Traffic spikes 10x. The same query โ€” fetch the top 100 products, join three tables, apply filters โ€” runs 50,000 times in a minute. Your database CPU hits 100%. Queries start queuing. Response times climb from 40ms to 4 seconds. Users see errors.

The query is not slow. Running it once takes 40ms. Running it 50,000 times in a minute against a single database instance is the problem. You are doing the same computation repeatedly, fetching data that has not changed, burning database connections on reads that could be served from memory.

Redis sits between your application and your database and solves exactly this. It stores the result of expensive operations in memory so subsequent requests get the answer in under a millisecond without touching the database at all.

This post explains how Redis works, what it is actually good at beyond caching, and when it is the wrong tool for the job.


๐ŸŽฏ Quick Answer (30-Second Read)

  • What it is: An in-memory data structure store used as a cache, message broker, and session store
  • Why companies use it: Sub-millisecond reads, reduces database load, handles rate limiting and session management elegantly
  • Main benefit: Serves repeated reads from memory โ€” your database only gets hit when data is not cached or has expired
  • Main limitation: Data lives in RAM โ€” not suited for large datasets or primary storage
  • Recommendation: Add Redis when you have hot data โ€” frequently read, rarely changed โ€” that your database is serving redundantly at high volume

How Redis Actually Works

Redis is an in-memory key-value store. Every piece of data has a key (a string) and a value (one of several data structures). Reads and writes happen entirely in RAM, which is why they are fast โ€” memory access is orders of magnitude faster than disk I/O or network round-trips to a database.

The basic mental model:

Client Request
โ†“
Check Redis cache for key
โ†“
Cache HIT โ†’ return value (< 1ms)
Cache MISS โ†’ query database โ†’ store result in Redis โ†’ return value
โ†“
Next request for same key โ†’ Cache HIT

This pattern is called cache-aside (or lazy loading). Your application checks Redis first. On a miss, it fetches from the source of truth, stores the result in Redis with a TTL (time-to-live), and returns it. Subsequent requests for the same key are served from Redis until the TTL expires.

Redis Data Structures

Redis is not just a string cache. It ships with native data structures that map directly to common application patterns:

Strings โ€” the basic key-value pair. Store serialised JSON, HTML fragments, computation results. Set with SET key value EX 3600 (expires in one hour).

Hashes โ€” a map of field-value pairs under one key. Store a user object as a hash and update individual fields without serialising and deserialising the whole object.

Lists โ€” ordered sequences. Use for activity feeds, job queues, or any ordered collection where you need efficient head/tail operations.

Sets โ€” unordered collections of unique values. Use for tracking unique visitors, tags, or membership checks. O(1) lookup regardless of set size.

Sorted Sets โ€” sets where each member has a score. The canonical use case is leaderboards โ€” insert scores, retrieve top-N by rank, in O(log n) time.

Streams โ€” an append-only log structure. Use for event sourcing, audit trails, or lightweight message passing between services.


What Companies Actually Use Redis For

Caching Database Queries

The primary use case. Cache the result of expensive or frequently repeated database queries with a TTL that matches how often the underlying data changes. A product listing that updates once an hour does not need to hit the database on every page load.

async function getProducts(categoryId) {
  const cacheKey = `products:category:${categoryId}`;
  const cached = await redis.get(cacheKey);

  if (cached) return JSON.parse(cached);

  const products = await db.query(
    "SELECT * FROM products WHERE category_id = $1",
    [categoryId]
  );

  await redis.setex(cacheKey, 3600, JSON.stringify(products));
  return products;
}

Session Storage

HTTP is stateless. To maintain user sessions across requests, you need to store session data somewhere. Storing it in your relational database works but adds a database read to every authenticated request. Redis handles this better โ€” fast reads, built-in TTL for session expiry, and horizontal scaling without session stickiness issues.

Rate Limiting

Redis's atomic increment operations make it the standard tool for rate limiting. The pattern: increment a counter for a user/IP key on each request, set a TTL on the key, reject requests once the counter exceeds the threshold.

async function rateLimit(userId) {
  const key = `rate_limit:${userId}`;
  const requests = await redis.incr(key);

  if (requests === 1) {
    await redis.expire(key, 60); // reset window every 60 seconds
  }

  return requests <= 100; // allow 100 requests per minute
}

This works because Redis operations are atomic โ€” no race conditions, no double-counting, no need for database transactions.

Pub/Sub and Message Queues

Redis supports publish/subscribe messaging and list-based job queues. For lightweight async task processing โ€” sending emails, resizing images, processing webhooks โ€” Redis-backed queues (using libraries like BullMQ) are simpler to operate than dedicated message brokers like RabbitMQ or Kafka for most application scales.

Leaderboards and Real-Time Counters

Sorted sets make leaderboards trivial. Add a user's score with ZADD leaderboard 1250 user:42, retrieve the top 10 with ZREVRANGE leaderboard 0 9 WITHSCORES. Updates are O(log n), reads are O(log n + result size). No complex SQL, no table locks, sub-millisecond at any reasonable leaderboard size.


The Right Approach vs The Wrong Approach

The right approach is treating Redis as a read-through cache for hot data with appropriate TTLs. Identify your most frequently read, rarely changed data โ€” product listings, user profiles, configuration, computed aggregates โ€” and cache it. Set TTLs that match the acceptable staleness window for each data type. Build cache invalidation logic that clears or updates the cache when the underlying data changes.

Monitor your cache hit rate. A well-configured cache should hit 80โ€“95% on hot data. If your hit rate is below 60%, your keys are too granular, your TTLs are too short, or you are caching data that is not actually read repeatedly.

The wrong approach is using Redis as a primary database. Redis stores data in memory โ€” RAM is expensive and finite. Persistence options exist (RDB snapshots, AOF logging) but they add complexity and Redis is not designed to be your source of truth. If your Redis instance crashes and you have not configured persistence correctly, you lose data.

The other wrong approach is caching everything with long TTLs and no invalidation strategy. Stale cache data that diverges from the database causes bugs that are hard to reproduce and painful to debug. Cache data that can tolerate staleness. Build explicit invalidation for data that cannot.


My Take

The reason Redis became the default caching layer for almost every production web application is that it solved a real problem with the simplest possible interface โ€” a key-value store with native data structures and atomic operations. The best outcome for a team adding Redis is a measurable reduction in database load and query latency, achieved with a small amount of well-placed caching logic around genuinely hot data. The worst outcome is a cache that nobody trusts because invalidation is broken โ€” engineers start bypassing it, the hit rate drops, and you are paying for Redis infrastructure that is not doing anything useful. Right now, the interesting pressure on Redis is from two directions: managed Postgres extensions like pgvector and Postgres-native caching strategies on one side, and cloud-native caches like DynamoDB Accelerator and Momento on the other. Redis still wins on flexibility and data structure richness, but its moat is narrower than it was five years ago. Where this is heading: Redis's role in the stack gets smaller as databases get faster and smarter about their own caching layers โ€” but for rate limiting, session storage, and pub/sub, it will be the right tool for a long time.


Comparison Table

Use Case Redis Database In-Memory App Cache
Hot read caching โœ“ Best Too slow at scale Doesn't survive restarts
Session storage โœ“ Best Works, adds load Single instance only
Rate limiting โœ“ Best Race conditions Single instance only
Leaderboards โœ“ Best Complex, slow at scale No persistence
Job queues โœ“ Good Works, polling overhead No persistence
Primary data storage โœ— Wrong tool โœ“ Best โœ— Wrong tool
Large dataset storage โœ— RAM limits โœ“ Best โœ— Wrong tool

Real Developer Use Case

A SaaS dashboard was running a complex aggregate query โ€” total events, active users, revenue this month โ€” on every page load for every user. The query joined four tables and took 280ms. At 200 concurrent users, database CPU was consistently above 70%.

The fix was a single Redis cache key per organisation, storing the pre-computed dashboard aggregate with a 5-minute TTL and explicit invalidation on write events:

const cacheKey = `dashboard:org:${orgId}`;
const TTL = 300; // 5 minutes

// On read
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const data = await computeDashboard(orgId);
await redis.setex(cacheKey, TTL, JSON.stringify(data));
return data;

// On write (new event, payment, etc.)
await redis.del(cacheKey);

Result: 94% cache hit rate. Database CPU dropped from 70% to 12%. Dashboard load time dropped from 280ms to under 3ms on cache hits. Engineering time: four hours.


Frequently Asked Questions

What is the difference between Redis and Memcached?

Both are in-memory caches. Memcached is simpler โ€” strings only, no persistence, no replication. Redis supports multiple data structures, optional persistence, pub/sub, Lua scripting, and cluster mode. For new projects, Redis is the default choice. Memcached makes sense only if you need maximum simplicity and have no use for Redis's additional features.

Does Redis persist data if the server restarts?

By default, Redis is volatile โ€” a restart loses all data. Persistence is optional: RDB snapshots write the dataset to disk periodically, AOF logging writes every write operation to an append-only file. For caching workloads, no persistence is fine โ€” a cold cache after restart warms up quickly. For session storage or queues, enable persistence.

How do I avoid stale cache data in Redis?

Use TTLs appropriate to how often your data changes. Build explicit cache invalidation โ€” delete or update the cache key whenever the underlying data changes. For data that must always be fresh, do not cache it in Redis. The discipline is in knowing which data tolerates staleness and which does not.

What is the difference between Redis and a CDN cache?

A CDN caches HTTP responses at the edge โ€” close to users geographically. Redis caches at the application layer โ€” close to your database. CDN caching works for public, cacheable HTTP responses (static assets, API responses with cache headers). Redis caches private, user-specific, or dynamically computed data that a CDN cannot cache.

Should I use Redis on a small project?

Not necessarily. If your database is handling the load without performance issues, adding Redis is operational overhead for no gain. Add it when you have a measurable problem โ€” high database load, slow repeated queries, rate limiting needs, or session management at scale. Redis is a solution to a specific problem, not a default component.


Conclusion

Redis is the standard caching layer for production web applications because it solves a specific problem elegantly: serving repeated reads from memory, at sub-millisecond latency, without touching the database.

Add it when you have hot data โ€” frequently read, rarely changed โ€” that your database is serving redundantly under load. Set appropriate TTLs. Build explicit invalidation. Monitor your hit rate.

Redis earns its place in the stack when it is solving a real problem. That problem usually becomes visible around the time your database starts struggling under read load โ€” which is exactly when you want the solution already in place.

Related reads: Why Apps Crash Under High Traffic ยท PostgreSQL vs MySQL: Which Database Should You Pick? ยท How to Create a SaaS with Next.js and Supabase