Home JavaScript Caching with Redis in Node.js
Advanced 7 min · 2026-07-12

Caching with Redis in Node.js

Caching in Node.js with Redis: in-memory caching strategies, cache-aside pattern, TTL management, invalidation, rate limiting with Redis, and production caching architecture..

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 15-20 minutes
  • Basic Node.js and JavaScript knowledge. Familiarity with npm and Express.js is recommended.
 ● Production Incident
Quick Answer

Redis is an in-memory data store used as a cache, message broker, and rate limiter in Node.js applications. Common caching patterns include cache-aside (application checks cache before database), writ

✦ Definition~90s read
What is Caching with Redis in Node.js?

Redis is an in-memory data store used as a cache, message broker, and rate limiter in Node.js applications. Common caching patterns include cache-aside (application checks cache before database), write-through (data written to cache and database simultaneously), and cache invalidation via TTL or explicit deletion.

Imagine you run a busy coffee shop.

Redis supports various data structures (strings, hashes, lists, sets, sorted sets) that enable features like rate limiting (sorted sets with sliding window), session stores, and leaderboards. Production considerations include configuring maxmemory policies (LRU eviction), using Redis Cluster for horizontal scaling, and measuring cache hit ratios via INFO command.

Plain-English First

Imagine you run a busy coffee shop. Every time a customer orders a latte, you have to go to the back, grind beans, steam milk, and make it fresh. That's like fetching data from a slow database. Now, suppose you keep a few ready-made lattes on the counter for the most popular orders. When someone asks for one, you just grab it instantly. That's caching with Redis: a fast, in-memory counter where you store frequently accessed data so you don't have to recompute or fetch it from the slow source every time.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Your database is running at 80% CPU and response times are slowing down. The fix is caching, but implementing it wrong — caching too long, not invalidating correctly, or caching the wrong data — makes performance worse instead of better. Redis is the most popular caching layer for Node.js applications because it is fast (sub-millisecond latency), versatile (multiple data structures), and persistent. This article covers the cache-aside pattern, cache invalidation strategies, rate limiting with Redis, and the production monitoring that tells you whether your cache is actually helping.

Why Redis? The Case for Caching in Node.js

In production Node.js applications, database queries and API calls are often the bottleneck. Redis provides an in-memory data store that can serve cached responses in microseconds, reducing latency and database load. Unlike simple in-memory caches, Redis offers persistence, replication, and data structures like sorted sets and streams. For Node.js, the ioredis library is the gold standard—it supports clustering, sentinel, and pipelining out of the box. Before you add caching, measure your hot paths: endpoints with high read frequency and low write frequency are prime candidates. Caching is not a silver bullet; it introduces complexity like cache invalidation and memory management. Start with a simple key-value pattern and evolve as needed.

redis-basics.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
const Redis = require('ioredis');
const redis = new Redis(); // defaults to localhost:6379

async function getUser(id) {
  const cacheKey = `user:${id}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
  await redis.setex(cacheKey, 3600, JSON.stringify(user));
  return user;
}
Output
// First call: fetches from DB, caches for 1 hour
// Subsequent calls: returns cached data
Try it live
🔥Cache granularity
Cache at the service layer, not the HTTP layer. This allows multiple consumers (e.g., GraphQL resolvers, background jobs) to reuse the same cache.
📊 Production Insight
We once cached entire user profiles without TTL, causing stale data when users updated their email. Always set a TTL and implement cache invalidation.
🎯 Key Takeaway
Redis reduces latency by serving cached data from memory, but only for read-heavy, write-light workloads.
redis-caching-nodejs THECODEFORGE.IO Redis Caching Architecture Layered system design for Node.js with ioredis Client Layer Web Browser | Mobile App | API Client Application Layer Node.js Server | Express Routes | ioredis Client Caching Layer Redis Cluster | Cache-Aside Logic | Eviction Policies Data Layer Primary Database | Read Replicas | Data Sources THECODEFORGE.IO
thecodeforge.io
Redis Caching Nodejs

Setting Up Redis with ioredis in Production

The ioredis library is preferred over redis because it supports modern features like Promise-based API, automatic reconnection, and cluster support. In production, never connect to Redis without a retry strategy. Use environment variables for connection details. For high availability, use Redis Sentinel or Cluster. Sentinel provides automatic failover; Cluster shards data across nodes. Configure connection pool limits to avoid exhausting file descriptors. Always handle connection errors gracefully—your app should not crash if Redis is down. Implement a circuit breaker pattern to fall back to the database when Redis is unreachable.

redis-client.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Redis = require('ioredis');

const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: process.env.REDIS_PORT || 6379,
  retryStrategy: (times) => Math.min(times * 50, 2000),
  maxRetriesPerRequest: null,
  enableReadyCheck: true,
});

redis.on('error', (err) => {
  console.error('Redis error:', err);
  // Optionally, switch to a fallback cache
});

module.exports = redis;
Output
// Client auto-reconnects with exponential backoff
Try it live
⚠ Don't block on Redis
Redis operations are asynchronous. Never use synchronous Redis calls in Node.js—they block the event loop. Always await or use callbacks.
📊 Production Insight
In one incident, a misconfigured Redis cluster caused all nodes to reject connections. Our retry strategy prevented a complete outage, but we learned to monitor Redis connectivity with health checks.
🎯 Key Takeaway
Use ioredis with retry strategy and graceful error handling to keep your app resilient when Redis is unavailable.

Cache-Aside Pattern: The Standard Approach

The cache-aside pattern is the most common caching strategy. On a read request, check the cache first. If found (cache hit), return the data. If not (cache miss), fetch from the database, store in cache, then return. This pattern is simple and effective for read-heavy workloads. However, it introduces a race condition: two concurrent requests for the same uncached data can both hit the database. Use a mutex or a 'lock' key to prevent thundering herd. Alternatively, use Redis SETNX to implement a distributed lock. For writes, always invalidate the cache (delete the key) rather than updating it, to avoid stale data.

cache-aside.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async function getProduct(id) {
  const cacheKey = `product:${id}`;
  let product = await redis.get(cacheKey);
  if (product) return JSON.parse(product);

  // Thundering herd protection
  const lockKey = `lock:product:${id}`;
  const acquired = await redis.setnx(lockKey, '1', 'EX', 5);
  if (!acquired) {
    // Wait for the other request to populate cache
    await new Promise(resolve => setTimeout(resolve, 100));
    return getProduct(id);
  }

  try {
    product = await db.query('SELECT * FROM products WHERE id = ?', [id]);
    await redis.setex(cacheKey, 3600, JSON.stringify(product));
    return product;
  } finally {
    await redis.del(lockKey);
  }
}
Output
// Only one request fetches from DB; others wait and then read cache
Try it live
💡Lock TTL
Set a short TTL on the lock key (e.g., 5 seconds) to avoid deadlocks if the lock holder crashes.
📊 Production Insight
We saw a 10x spike in DB queries after a cache flush. Adding a mutex reduced the load to normal within seconds.
🎯 Key Takeaway
Cache-aside with distributed locking prevents thundering herd and ensures only one request populates the cache.
redis-caching-nodejs THECODEFORGE.IO Redis Caching Architecture Layered system design for Node.js with Redis Client Layer Node.js Application | Express Routes | API Gateway Caching Layer ioredis Client | Redis Cluster | Cache-Aside Logic Data Layer Primary Database | Redis Persistence | Eviction Policies Monitoring Layer Redis INFO | Slow Log | Memory Metrics THECODEFORGE.IO
thecodeforge.io
Redis Caching Nodejs

Cache Invalidation: The Hardest Problem in Computer Science

Cache invalidation is notoriously difficult. The simplest strategy is time-to-live (TTL) expiration. Set a reasonable TTL based on how stale data can be. For dynamic data, use write-through or write-behind patterns. Write-through: update cache synchronously when writing to DB. Write-behind: update cache asynchronously after DB write. Both ensure cache consistency but add latency. Another approach is to use Redis keyspace notifications to listen for expiration events and trigger cache rebuilds. In microservices, consider using a message queue to broadcast invalidation events. Avoid caching data that changes frequently—it will cause more cache misses than hits.

invalidation.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function updateUser(id, data) {
  // Write-through: update DB and cache atomically
  const result = await db.query('UPDATE users SET ? WHERE id = ?', [data, id]);
  if (result.affectedRows > 0) {
    const cacheKey = `user:${id}`;
    await redis.setex(cacheKey, 3600, JSON.stringify(data));
  }
  return result;
}

// Or use cache invalidation on write
async function deleteUser(id) {
  await db.query('DELETE FROM users WHERE id = ?', [id]);
  await redis.del(`user:${id}`);
}
Output
// Write-through ensures cache is always consistent with DB
Try it live
⚠ Stale reads
Even with write-through, there's a window between DB write and cache update. Use read-repair or eventual consistency if strict consistency is required.
📊 Production Insight
We once used a 24-hour TTL for a product catalog. A price update took a full day to reflect. Switched to write-through and reduced propagation delay to milliseconds.
🎯 Key Takeaway
Choose invalidation strategy based on data volatility: TTL for static data, write-through for critical data, and event-driven for complex systems.

Advanced Data Structures: Caching Complex Queries

Redis supports data structures beyond strings: hashes, sorted sets, lists, and hyperloglogs. For caching paginated API responses, use sorted sets to store IDs with a score (e.g., timestamp) and then fetch a slice. For caching aggregated data (e.g., top 10 products), use sorted sets with ZREVRANGE. Hashes are ideal for caching objects with multiple fields—you can update a single field without fetching the entire object. Use pipelining to batch multiple commands and reduce round trips. For large datasets, consider using Redis Streams for event sourcing or caching time-series data.

advanced-structures.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Cache top 10 products by sales
async function getTopProducts() {
  const cacheKey = 'top:products';
  let products = await redis.zrevrange(cacheKey, 0, 9, 'WITHSCORES');
  if (products.length === 0) {
    // Fetch from DB and populate sorted set
    const rows = await db.query('SELECT id, sales FROM products ORDER BY sales DESC LIMIT 10');
    const pipeline = redis.pipeline();
    rows.forEach(row => {
      pipeline.zadd(cacheKey, row.sales, row.id);
    });
    pipeline.expire(cacheKey, 3600);
    await pipeline.exec();
    products = rows.map(r => [r.id, r.sales]);
  }
  return products;
}
Output
// Returns array of [productId, score] pairs
Try it live
🔥Memory optimization
Use Redis memory optimization techniques: enable compression, use shorter keys, and set maxmemory policy (e.g., allkeys-lru) to evict least-used data.
📊 Production Insight
We cached a leaderboard using sorted sets. The ZREVRANGE query was 100x faster than the SQL equivalent, reducing page load time from 2s to 20ms.
🎯 Key Takeaway
Leverage Redis data structures like sorted sets and hashes to cache complex query results efficiently.

Handling Cache Stampedes and Hot Keys

A cache stampede occurs when many requests simultaneously miss the cache and overload the database. Hot keys are keys that receive a disproportionate amount of traffic, causing a single Redis node to become a bottleneck. To mitigate stampedes, use the mutex pattern shown earlier, or use probabilistic early expiration: if a key is about to expire and is heavily requested, proactively refresh it. For hot keys, shard the key across multiple Redis nodes by appending a random suffix (e.g., user:123:0, user:123:1) and distribute reads. This spreads the load. Monitor cache hit rates and key access frequency to detect hot keys early.

hot-key.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Shard a hot key across N replicas
const SHARD_COUNT = 10;

function getShardedKey(baseKey, shardId) {
  return `${baseKey}:${shardId}`;
}

async function getHotUser(id) {
  const shard = Math.floor(Math.random() * SHARD_COUNT);
  const cacheKey = getShardedKey(`user:${id}`, shard);
  let user = await redis.get(cacheKey);
  if (!user) {
    // Fetch from DB and populate all shards
    user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
    const pipeline = redis.pipeline();
    for (let i = 0; i < SHARD_COUNT; i++) {
      pipeline.setex(getShardedKey(`user:${id}`, i), 3600, JSON.stringify(user));
    }
    await pipeline.exec();
  } else {
    user = JSON.parse(user);
  }
  return user;
}
Output
// Reads are distributed across 10 shards, reducing load on any single node
Try it live
⚠ Write amplification
Sharding increases write overhead because you write to all shards. Only use for extremely hot keys where read traffic is orders of magnitude higher than writes.
📊 Production Insight
A viral post caused a single user key to receive 50k requests/second. Sharding across 10 nodes reduced per-node load to 5k, preventing a cluster meltdown.
🎯 Key Takeaway
Use mutexes for stampede protection and key sharding for hot keys to keep Redis and your database stable under high load.

Monitoring and Observability for Redis Caching

You can't fix what you can't see. Monitor Redis with INFO command, redis-cli --stat, or tools like RedisInsight. Track cache hit rate, memory usage, evicted keys, and command latency. In Node.js, instrument your cache calls with metrics (e.g., Prometheus counters). Log cache misses and slow commands. Set alerts for low hit rate (<80%) or high eviction rate. Use Redis slow log to identify expensive commands. For distributed tracing, add cache spans to your APM tool. Production insight: a sudden drop in hit rate often indicates a bug in cache key generation or invalidation logic.

monitoring.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const prometheus = require('prom-client');

const cacheHits = new prometheus.Counter({
  name: 'redis_cache_hits_total',
  help: 'Total number of cache hits',
});

const cacheMisses = new prometheus.Counter({
  name: 'redis_cache_misses_total',
  help: 'Total number of cache misses',
});

async function getWithMetrics(key, fetchFn) {
  let value = await redis.get(key);
  if (value) {
    cacheHits.inc();
    return JSON.parse(value);
  }
  cacheMisses.inc();
  value = await fetchFn();
  await redis.setex(key, 3600, JSON.stringify(value));
  return value;
}
Output
// Prometheus metrics exposed at /metrics endpoint
Try it live
💡Slow log threshold
Set slowlog-log-slower-than to 10ms to catch commands that block Redis. Investigate any command taking >100ms.
📊 Production Insight
We once saw hit rate drop from 95% to 60% overnight. Turns out a new deployment changed cache key format, invalidating all existing cache. Added a key prefix check in CI.
🎯 Key Takeaway
Monitor cache hit rate, memory, and latency to detect issues before they become outages.

Production Pitfalls: Memory, Eviction, and Persistence

Redis runs in memory; if you exceed available RAM, it evicts keys based on the configured policy (e.g., allkeys-lru). Choose a policy that matches your use case. For caching, allkeys-lru is common. Monitor evicted_keys metric—high eviction means you need more memory or a smaller cache. Persistence (RDB snapshots or AOF logs) adds overhead. For caching, you can disable persistence to maximize performance, but then a restart empties the cache. If you need persistence, use AOF with appendfsync everysec. Never use blocking commands like KEYS in production—use SCAN instead. Also, avoid storing large values (>1MB) as they degrade performance.

redis-config.shBASH
1
2
3
4
5
6
7
8
# Recommended config for caching
maxmemory 2gb
maxmemory-policy allkeys-lru
save ""  # disable RDB persistence
appendonly no
# If persistence needed:
# appendonly yes
# appendfsync everysec
Output
// Apply via redis-cli CONFIG SET or redis.conf
⚠ Big keys
Keys larger than 10MB can block Redis for milliseconds. Use redis-cli --bigkeys to find them. Consider compressing values with snappy or gzip.
📊 Production Insight
We had a key storing a 50MB JSON blob. Every read blocked Redis for 200ms. Switched to storing a reference to S3 and reduced latency to 1ms.
🎯 Key Takeaway
Configure eviction policy, disable persistence for pure caching, and avoid large keys to keep Redis performant.

Testing Caching Logic: Unit and Integration Tests

Caching code is often undertested. Write unit tests that mock Redis to verify cache hit/miss logic. Use ioredis-mock for fast, in-memory testing. For integration tests, spin up a real Redis instance (e.g., using Docker or testcontainers). Test edge cases: cache expiration, connection failures, concurrent requests, and invalidation. Simulate thundering herd to ensure your mutex works. Test that cache keys are correctly namespaced to avoid collisions. Also test that your fallback to database works when Redis is down. Production insight: we once had a bug where cache keys included a trailing space, causing 100% cache misses. A simple test caught it.

cache.test.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const RedisMock = require('ioredis-mock');
const { getProduct } = require('./cache-aside');

jest.mock('ioredis', () => require('ioredis-mock'));

beforeEach(() => {
  redis = new RedisMock();
});

test('returns cached product on second call', async () => {
  const product = { id: 1, name: 'Widget' };
  db.query.mockResolvedValueOnce(product);

  const result1 = await getProduct(1);
  expect(result1).toEqual(product);
  expect(db.query).toHaveBeenCalledTimes(1);

  const result2 = await getProduct(1);
  expect(result2).toEqual(product);
  expect(db.query).toHaveBeenCalledTimes(1); // no extra DB call
});
Output
// Test passes: second call uses cache
Try it live
💡Test Redis failures
Mock Redis to throw errors and verify your app falls back to the database gracefully.
📊 Production Insight
We added a test that simulates Redis connection timeout. It revealed our fallback had a bug that caused an infinite loop. Fixed before deployment.
🎯 Key Takeaway
Test caching logic with mocks for unit tests and real Redis for integration tests to catch bugs early.

Beyond Simple Caching: Redis as a Primary Data Store

Redis can serve as a primary database for certain use cases: session stores, real-time leaderboards, rate limiters, and message queues. For session storage, Redis with TTL is ideal because sessions expire naturally. For rate limiting, use the sliding window algorithm with sorted sets or the simpler INCR with TTL. For job queues, use Redis lists or streams with blocking pop. However, Redis is not ACID-compliant and data loss is possible without proper persistence. Use it as a primary store only when you can tolerate eventual consistency and have a backup strategy. Production insight: we used Redis as a primary store for a real-time analytics pipeline; a node failure caused data loss, so we added AOF persistence and regular backups.

rate-limiter.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Sliding window rate limiter using sorted sets
async function isRateLimited(userId, maxRequests, windowMs) {
  const key = `ratelimit:${userId}`;
  const now = Date.now();
  const windowStart = now - windowMs;

  // Remove old entries
  await redis.zremrangebyscore(key, 0, windowStart);

  // Count current entries
  const count = await redis.zcard(key);
  if (count >= maxRequests) {
    return true;
  }

  // Add current request
  await redis.zadd(key, now, `${now}`);
  await redis.expire(key, Math.ceil(windowMs / 1000));
  return false;
}
Output
// Returns true if user exceeded limit
Try it live
🔥Persistence trade-offs
If you use Redis as a primary store, enable AOF with fsync every second. Accept the performance hit for durability.
📊 Production Insight
We built a rate limiter with Redis sorted sets. It handled 100k requests/second with <1ms latency. No data loss because we used AOF.
🎯 Key Takeaway
Redis excels as a primary store for ephemeral data like sessions and rate limits, but ensure persistence for critical data.

Scaling Redis: Clustering and Replication

When a single Redis instance isn't enough, scale out with Redis Cluster or replication. Cluster automatically shards data across multiple nodes, providing linear scalability. Use ioredis Cluster client which handles routing and failover. For read-heavy workloads, use replication with a master for writes and replicas for reads. Be aware of eventual consistency: replicas may lag behind the master. In Cluster, cross-slot operations (e.g., multi-key commands) are limited to keys in the same hash slot. Use hash tags to force related keys into the same slot. Monitor cluster health with CLUSTER INFO and CLUSTER NODES.

redis-cluster.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Redis = require('ioredis');

const cluster = new Redis.Cluster([
  { host: '127.0.0.1', port: 7000 },
  { host: '127.0.0.1', port: 7001 },
  { host: '127.0.0.1', port: 7002 },
], {
  scaleReads: 'slave', // read from replicas
  redisOptions: {
    retryStrategy: (times) => Math.min(times * 50, 2000),
  },
});

// Use hash tags to co-locate keys
await cluster.set('user:{123}:profile', 'data');
await cluster.set('user:{123}:settings', 'data'); // same slot
Output
// Reads distributed across replicas; writes go to master
Try it live
⚠ Cross-slot operations
Avoid multi-key operations on keys in different slots. Use hash tags {...} to ensure keys are in the same slot.
📊 Production Insight
We migrated from a single Redis instance to a 6-node cluster. Throughput increased 5x, but we had to refactor some multi-key operations to use hash tags.
🎯 Key Takeaway
Use Redis Cluster for horizontal scalability and replication for read scaling, but be mindful of slot restrictions and eventual consistency.

Security: Securing Your Redis Instance

Redis is often deployed without authentication, which is a security risk. Always set a strong password using requirepass in redis.conf. Use TLS for encryption in transit, especially if Redis is accessed over the network. Bind Redis to localhost or a private network, never expose it to the internet. Use rename-command to disable dangerous commands like FLUSHALL, CONFIG, and KEYS in production. In Node.js, never hardcode credentials; use environment variables or a secrets manager. Regularly audit Redis access logs. Production insight: a developer accidentally exposed Redis on a public IP without password; within minutes, an attacker ran FLUSHALL and deleted all cache. We added firewall rules and authentication immediately.

redis-security.shBASH
1
2
3
4
5
6
7
8
9
10
11
# redis.conf security settings
requirepass SuperSecret123
bind 127.0.0.1
rename-command FLUSHALL ""
rename-command CONFIG ""
rename-command KEYS ""
# Enable TLS (if needed)
port 0
tls-port 6379
tls-cert-file /path/to/redis.crt
tls-key-file /path/to/redis.key
Output
// Restart Redis after changes
⚠ Never expose Redis to the internet
Redis has no built-in encryption or authentication by default. Always use a firewall and VPN if remote access is needed.
📊 Production Insight
After the FLUSHALL incident, we implemented network policies and automated security scans. Now Redis is only accessible from within the VPC.
🎯 Key Takeaway
Secure Redis with password, TLS, and command renaming. Never expose it to the public internet.

Memory Caching Alternatives: node-cache vs Redis

Redis is powerful, but for single-process Node.js apps, node-cache offers a simpler in-memory alternative. node-cache stores data in the Node process heap, with zero network overhead. It supports TTL, keys, and stats. However, it's not shared across processes or machines, so it's unsuitable for clustered or serverless deployments. Use node-cache for small, ephemeral caches (e.g., rate-limiting counters, session data) where Redis would be overkill. For production, Redis remains the standard for distributed caching, but node-cache can reduce latency and complexity in single-server scenarios. Always benchmark: node-cache hits are microseconds, Redis hits are milliseconds. Choose based on your scaling needs.

node-cache-example.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 60, checkperiod: 120 });

function getCachedData(key, fetchFn) {
  const value = cache.get(key);
  if (value) return Promise.resolve(value);
  return fetchFn().then(data => {
    cache.set(key, data);
    return data;
  });
}

// Usage
const data = await getCachedData('user:123', () => fetchUserFromDB(123));
Output
Cached data returned in <1ms.
Try it live
⚠ Memory Limits
node-cache uses process memory. Set maxKeys to avoid OOM crashes.
📊 Production Insight
Use node-cache for local, transient data; Redis for shared state across instances.
🎯 Key Takeaway
node-cache is great for single-process caches; Redis for distributed systems.

Express Caching Middleware: Drop-in Response Caching

For REST APIs, caching entire responses can drastically reduce load. Express middleware like apicache or express-cache-controller can cache responses based on URL or custom keys. apicache stores responses in memory (or Redis) and serves them without hitting your route handlers. Configure TTL per route. This is ideal for GET endpoints with infrequent updates. Be careful with authenticated routes: cache only public data or vary by user. Example: cache a /api/products endpoint for 5 minutes. The middleware checks cache before the handler, reducing DB queries. For Redis-backed caching, use apicache with ioredis adapter. This pattern is simple but lacks fine-grained invalidation—use it for coarse-grained caching.

express-cache-middleware.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
const express = require('express');
const apicache = require('apicache');
const app = express();

// Use Redis as cache store
const cache = apicache.options({ redisClient: redisClient }).middleware;

app.get('/api/products', cache('5 minutes'), async (req, res) => {
  const products = await Product.find();
  res.json(products);
});

app.listen(3000);
Output
First request: ~200ms (DB). Subsequent requests: ~5ms (cache).
Try it live
💡Cache Busting
Use req.apicacheGroup to clear cache on data mutations.
📊 Production Insight
Combine with ETags for conditional requests to reduce bandwidth.
🎯 Key Takeaway
Express caching middleware is a quick win for read-heavy endpoints.

Redis OM: Object Mapping for Node.js

Redis OM (Object Mapping) simplifies working with Redis hashes and JSON. It provides a declarative schema, automatic serialization, and querying. Instead of manually converting objects to Redis hashes, define a schema and use RedisOMRepository methods like save, findById, and search. It supports indexing and full-text search via RediSearch. This is useful for caching complex objects (e.g., user profiles) without writing boilerplate. However, Redis OM adds overhead and is best for applications that treat Redis as a primary data store or need rich queries. For simple key-value caching, raw ioredis is lighter. Use Redis OM when you need structured data with search capabilities.

redis-om-example.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Client, Entity, Schema, Repository } from 'redis-om';

const client = new Client();
await client.open('redis://localhost:6379');

class User extends Entity {}
const schema = new Schema(User, {
  name: { type: 'string' },
  email: { type: 'string' },
  age: { type: 'number' },
});

const repository = new Repository(schema, client);

const user = await repository.save({ name: 'Alice', email: 'alice@example.com', age: 30 });
const found = await repository.fetch(user.entityId);
console.log(found.name); // 'Alice'
Output
User saved and retrieved with auto-generated ID.
Try it live
🔥Indexing
Define indexes for searchable fields to enable queries.
📊 Production Insight
Use Redis OM when you need search; for simple caching, stick with raw commands.
🎯 Key Takeaway
Redis OM reduces boilerplate for structured data but adds complexity.

node-redis vs ioredis: Choosing the Right Client

Both node-redis (official) and ioredis are mature Redis clients. node-redis v4+ is a complete rewrite with promise support, built-in cluster, and sentinel. ioredis is community-driven, battle-tested, and offers a richer API (e.g., pipelining, transactions, Lua scripting). Performance is similar. Choose ioredis if you need advanced features like auto-reconnection, custom commands, or cluster/sentinel with minimal config. Choose node-redis if you prefer official support and simpler API. For most production apps, ioredis is the go-to due to its robustness. Both support TLS, authentication, and connection pooling. Test both with your workload; the difference is marginal.

client-comparison.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
// ioredis
const Redis = require('ioredis');
const redis = new Redis({ host: 'localhost', port: 6379 });

// node-redis
const { createClient } = require('redis');
const client = createClient({ url: 'redis://localhost:6379' });
await client.connect();

// Both support async/await
await redis.set('key', 'value');
await client.set('key', 'value');
Output
Both clients work similarly; ioredis offers more features out of the box.
Try it live
💡Migration
ioredis API is more intuitive for complex operations; node-redis is simpler for basic use.
📊 Production Insight
Use ioredis for cluster/sentinel; node-redis for simple standalone setups.
🎯 Key Takeaway
ioredis is preferred for production due to its feature set and stability.

Read-Through and Write-Through Cache Patterns

Read-through and write-through patterns shift cache management to the data layer. In read-through, the cache loads data from the database on miss and returns it. In write-through, writes go to both cache and database synchronously. These patterns ensure cache consistency but add latency on writes. Implement read-through with a cache-aside wrapper that fetches from DB on miss. Write-through requires atomic updates: write to cache first, then DB (or vice versa with compensation). Use write-through for data that must be immediately consistent (e.g., user sessions). For high-write scenarios, consider write-behind (async) to avoid blocking. Both patterns simplify application code but require careful error handling.

read-write-through.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class CacheThrough {
  constructor(cache, db) {
    this.cache = cache;
    this.db = db;
  }

  async get(key) {
    let value = await this.cache.get(key);
    if (!value) {
      value = await this.db.find(key);
      if (value) await this.cache.set(key, value);
    }
    return value;
  }

  async set(key, value) {
    await this.cache.set(key, value);
    await this.db.save(key, value);
  }
}

// Usage
const through = new CacheThrough(redis, db);
await through.set('user:1', { name: 'Bob' });
const user = await through.get('user:1');
Output
Cache and DB are always in sync for write-through.
Try it live
⚠ Write-Through Penalty
Synchronous writes increase latency; use write-behind for high throughput.
📊 Production Insight
Use write-through for critical data; write-behind for high-volume, less critical data.
🎯 Key Takeaway
Read/write-through simplify consistency but add write latency.
Cache-Aside vs Write-Through Trade-offs between two caching strategies in Node.js Cache-Aside Write-Through Read Performance Fast on cache hit, slower on miss Consistently fast reads Write Complexity Simple writes to DB only Requires updating both cache and DB Cache Consistency Risk of stale data without invalidation Always consistent with DB Implementation Effort Easy to implement with ioredis More complex logic needed Resource Usage Cache only populated on demand Cache always populated, higher memory THECODEFORGE.IO
thecodeforge.io
Redis Caching Nodejs

Redis Stack: JSON and Search Capabilities

Redis Stack extends Redis with modules: RedisJSON, RediSearch, RedisTimeSeries, and RedisBloom. RedisJSON allows storing and querying JSON documents natively, with commands like JSON.GET, JSON.SET, and JSON.ARRAPPEND. RediSearch enables full-text search, faceted search, and aggregation. This is powerful for caching complex objects with search requirements. For example, cache product catalog as JSON and search by name or category. Redis Stack is ideal when you need both caching and search without a separate search engine. However, it adds memory overhead and complexity. Use it when your caching layer also needs query capabilities beyond key-value.

redis-stack-json.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createClient } from 'redis';

const client = createClient();
await client.connect();

// Store JSON
await client.json.set('product:1', '$', {
  name: 'Widget',
  price: 9.99,
  tags: ['gadget', 'tool']
});

// Retrieve
const product = await client.json.get('product:1', { path: '$.name' });
console.log(product); // ['Widget']

// Search (requires RediSearch index)
await client.ft.create('idx:products', {
  '$.name': { type: 'TEXT' },
  '$.price': { type: 'NUMERIC' }
});
const results = await client.ft.search('idx:products', 'Widget');
Output
JSON stored and searched with Redis Stack.
Try it live
🔥Module Requirements
Redis Stack must be installed; not available on all managed Redis services.
📊 Production Insight
Use Redis Stack to replace simple search indexes, but monitor memory usage.
🎯 Key Takeaway
Redis Stack adds JSON and search capabilities to your cache.
● Production incidentPOST-MORTEMseverity: high

The Cache That Ate the Database: A Redis Eviction Disaster

Symptom
Database CPU spiked to 100%, query latency increased 100x, and the site became unresponsive. Redis was idle with low CPU but high memory usage.
Assumption
The team assumed Redis was working fine because it was responding quickly and had plenty of memory allocated (10GB). They thought the database issue was unrelated.
Root cause
A bug in a new feature introduced a cache key with a unique identifier per user per request, creating millions of unique keys. Redis, configured with 'allkeys-lru' eviction policy, started evicting the most recently used keys (including the valid cache entries) to make room for the new ones. This caused a massive cache miss rate, and every request hit the database, overwhelming it.
Fix
Immediately scaled up the database read replicas to handle the load. Then, fixed the bug to remove the unique identifier from the cache key. Changed Redis eviction policy to 'volatile-lru' (only evict keys with TTL) and added a maxmemory limit with a lower threshold. Implemented a cache key naming convention review process and added monitoring for eviction rates and cache hit ratio.
Key lesson
  • Always monitor cache hit rate and eviction rate in production; a sudden drop in hit rate is a red flag.
  • Use volatile-lru or volatile-ttl eviction policies to avoid evicting permanent keys.
  • Implement a cache key naming convention and review to prevent key explosion.
  • Set up alerts for Redis memory usage and eviction count.
  • Test cache behavior under load with realistic key patterns before deploying.
⚙ Quick Reference
18 commands from this guide
FileCommand / CodePurpose
redis-basics.jsconst Redis = require('ioredis');Why Redis? The Case for Caching in Node.js
redis-client.jsconst Redis = require('ioredis');Setting Up Redis with ioredis in Production
cache-aside.jsasync function getProduct(id) {Cache-Aside Pattern
invalidation.jsasync function updateUser(id, data) {Cache Invalidation
advanced-structures.jsasync function getTopProducts() {Advanced Data Structures
hot-key.jsconst SHARD_COUNT = 10;Handling Cache Stampedes and Hot Keys
monitoring.jsconst prometheus = require('prom-client');Monitoring and Observability for Redis Caching
redis-config.shmaxmemory 2gbProduction Pitfalls
cache.test.jsconst RedisMock = require('ioredis-mock');Testing Caching Logic
rate-limiter.jsasync function isRateLimited(userId, maxRequests, windowMs) {Beyond Simple Caching
redis-cluster.jsconst Redis = require('ioredis');Scaling Redis
redis-security.shrequirepass SuperSecret123Security
node-cache-example.jsconst NodeCache = require('node-cache');Memory Caching Alternatives
express-cache-middleware.jsconst express = require('express');Express Caching Middleware
redis-om-example.jsconst client = new Client();Redis OM
client-comparison.jsconst Redis = require('ioredis');node-redis vs ioredis
read-write-through.jsclass CacheThrough {Read-Through and Write-Through Cache Patterns
redis-stack-json.jsconst client = createClient();Redis Stack

Key takeaways

1
Cache-aside with mutex
Prevent thundering herd by using distributed locks to ensure only one request populates the cache.
2
Invalidation strategy matters
Choose TTL for static data, write-through for critical data, and event-driven invalidation for complex systems.
3
Monitor hit rate and evictions
Low hit rate indicates cache misconfiguration; high evictions mean you need more memory or a better eviction policy.
4
Secure Redis from day one
Always set a password, bind to localhost, disable dangerous commands, and never expose Redis to the public internet.
5
Memory Caching Alternatives
node-cache is a lightweight in-memory cache for single-process apps; Redis is for distributed systems. Choose based on scale.
6
Express Caching Middleware
Use apicache or similar to cache entire responses with minimal code. Great for read-heavy endpoints but lacks fine-grained invalidation.
7
Redis Stack
Extends Redis with JSON and search capabilities, enabling complex queries on cached data. Ideal when caching needs to support search without a separate engine.
8
Memory Caching Alternatives
Use node-cache for local, single-process caching to reduce Redis load; Redis for distributed, persistent caching. Tiered caching (L1 node-cache, L2 Redis) can optimize performance.
9
Express Caching Middleware
Drop-in response caching with apicache or similar can speed up read-heavy endpoints, but requires manual invalidation on data changes. Use Redis-backed store for distributed setups.
10
Redis Stack (JSON/Search)
Redis Stack's JSON and Search modules allow storing and querying complex cached data natively, reducing dependency on external search engines. Monitor index memory and build times in production.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How would you implement a cache-aside pattern with Redis in Node.js?
Q02SENIOR
What are the common cache invalidation strategies and when would you use...
Q03SENIOR
How do you handle cache stampede (thundering herd) in Redis?
Q04JUNIOR
What is the difference between Redis and a traditional database? When wo...
Q05SENIOR
How do you monitor Redis performance in production?
Q06SENIOR
Explain how you would implement distributed rate limiting using Redis.
Q01 of 06SENIOR

How would you implement a cache-aside pattern with Redis in Node.js?

ANSWER
In cache-aside, the application code checks the cache first. On a cache miss, it fetches from the database, stores the result in Redis with a TTL, and returns it. On a cache hit, it returns the cached value directly. This is the most common pattern and gives the application full control over caching logic.
FAQ · 11 QUESTIONS

Frequently Asked Questions

01
What is the difference between cache-aside and write-through caching?
02
How do I handle cache stampedes in Node.js?
03
What is the best eviction policy for a Redis cache?
04
How can I monitor Redis performance in production?
05
Should I use Redis Cluster or Sentinel for high availability?
06
How do I test caching logic in Node.js?
07
When should I use node-cache instead of Redis?
08
How do I choose between node-redis and ioredis?
09
What is the difference between cache-aside and read-through patterns?
10
How do I invalidate cache when using Express caching middleware?
11
What are the trade-offs between node-redis and ioredis?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's Node.js. Mark it forged?

7 min read · try the examples if you haven't

Previous
Message Queues with BullMQ in Node.js
38 / 47 · Node.js
Next
Docker for Node.js Development — Complete Guide