Redis In-Memory Caching
Why Redis is so fast, how in-memory caching works, and a step-by-step implementation guide.
Outline↓
Redis In-Memory Caching
Why database caching is used
Standard databases (like PostgreSQL, MySQL, and MongoDB) write and save data to physical Solid State Drives (SSDs) or Hard Drives.
While disk storage is excellent for keeping data safe when a server restarts, disk read/write speeds are slow. Opening files and querying disk indexes requires significant operating system overhead. If your backend gets 10,000 requests per minute and executes the exact same SQL queries for page layouts or config details over and over, your SQL database CPU usage will spike to 100%, causing your website to load slowly.
Disk Query:
Request ---> Backend Controller ---> SSD Disk Read (Slow: 80ms) ---> Respond
Redis was created to act as a hyper-fast caching layer that bypasses disk access completely.
Mental Model
Think of database storage like an office setup:
- SQL Database (The Filing Cabinet): It is locked in the corner of the room. Every time you need a file, you must get up, walk to the cabinet, unlock the drawer, search through index tabs, pull the paper, and sit back down. This takes time (analogous to Disk Read).
- Redis (The Desk): It is a notepad sitting directly on top of your desk. If you need a phone number, you look down at the pad and read it instantly. It takes a fraction of a second (analogous to RAM / In-Memory Read).
Because RAM space is limited and expensive, you only keep files you use frequently on your desk. The rest stays in the cabinet.
Core Specifications
- Why it exists: It was created to act as a high-speed key-value data structure store that processes queries in RAM rather than reading from slow physical disks.
- What problem it solves: It prevents database overload and high endpoint latencies by cache-aside querying, shielding databases from repetitive, identical read queries.
- How it works internally: It runs a single-threaded event loop utilizing non-blocking multiplexed socket connections. This architecture processes commands sequentially, avoiding CPU thread context-switching and data locking overhead.
- When to use it: Use it when building distributed session stores, API rate limiters, search query caching layers, chat pub/sub pipelines, and leaderboards requiring sub-millisecond response speeds.
- How it is used in real projects: Applications implement sliding-window rate limiters by storing client IP records as keys, executing atomic increments (
INCR), and setting key expirations (EXPIRE) to reject brute force requests.
Codebase Integration
Here is how to implement the standard Cache-Aside Caching Pattern using TypeScript and Redis.
Step 1: Install Dependencies
Install the official Redis client library and PostgreSQL library.
# In your project root
npm install redis pg
npm install @types/pg --save-dev
Step 2: Establish Redis Client Connection
Create a single database connection file. Put this in src/lib/redis.ts so your application reuses a single client connection.
// filepath: src/lib/redis.ts
// Purpose: Initialize and export a single connection to the Redis server.
import { createClient } from 'redis';
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
export const redis = createClient({
url: redisUrl,
});
redis.on('error', (err) => console.error('Redis Client Connection Error', err));
// Connect on server launch
if (!redis.isOpen) {
redis.connect().then(() => console.log('⚡ Connected to Redis Server'));
}
Step 3: Implement Caching Logic in Controllers
Now configure your controller routes to check the cache before hitting the main SQL database.
// filepath: src/controllers/userController.ts
// Purpose: Fetch user profile data, reading from Redis cache first.
import { Request, Response } from 'express';
import { redis } from '../lib/redis';
import { db } from '../lib/db'; // hypothetical PostgreSQL client pool
export async function getUserProfile(req: Request, res: Response) {
const { userId } = req.params;
const cacheKey = `user:profile:${userId}`;
try {
// 1. Query Redis Cache (Desk)
const cachedProfile = await redis.get(cacheKey);
if (cachedProfile) {
console.log('⚡ Cache Hit! Bypassing SQL database.');
return res.json(JSON.parse(cachedProfile));
}
// 2. Cache Miss: Query SQL Database (Filing Cabinet)
console.log('🐢 Cache Miss. Executing SQL query.');
const user = await db.query('SELECT id, name, email FROM users WHERE id = $1', [userId]);
if (!user.rows[0]) {
return res.status(404).json({ error: 'User profile not found' });
}
const profileData = user.rows[0];
// 3. Write copy back to Redis Cache (Desk)
// EX: 3600 sets a Time-To-Live (TTL) of 3600 seconds (1 hour)
await redis.set(cacheKey, JSON.stringify(profileData), {
EX: 3600,
});
return res.json(profileData);
} catch (error) {
console.error('Error fetching profile', error);
return res.status(500).json({ error: 'Internal Server Error' });
}
}
Practical Project Use Cases
1. Sliding Window Rate Limiter
Protecting public API endpoints from brute-force scripts or DDoS attacks.
- Real-World Example: We write a rate-limiting middleware that uses Redis atomic increments (
INCR) and expirations (EXPIRE) to limit clients to 100 requests per minute based on their IP address:
// filepath: src/middlewares/rateLimiter.ts
// Purpose: Enforce IP-based API rate limiting using atomic Redis counters.
import { Request, Response, NextFunction } from 'express';
import { redis } from '../lib/redis';
export async function rateLimit(req: Request, res: Response, next: NextFunction) {
const ip = req.ip;
const limitKey = `rate:limit:${ip}`;
// 1. Increment client count atomically
const currentHits = await redis.incr(limitKey);
// 2. If it is the first hit in this window, set TTL to 60 seconds
if (currentHits === 1) {
await redis.expire(limitKey, 60);
}
// 3. Enforce limit threshold
if (currentHits > 100) {
return res.status(429).json({
error: "Too many requests. Please try again in 1 minute."
});
}
next();
}
2. Distributed Session Store
Sharing user login sessions across a load-balanced cluster of servers.
- Real-World Example: In standard setups, Server A does not know about cookies signed by Server B. By storing session strings in a shared Redis cluster (using key
session:token_uuid), any server instance in the cluster can validate the incoming session in under 1ms, enabling seamless scaling.
Common Pitfalls
1. Forgetting to Set a Time-to-Live (TTL)
If you cache data without setting a TTL (EX / Expiry), it resides in RAM forever. If a user updates their name in the SQL database, your application will continue to read the outdated cached name from Redis indefinitely.
- Fix: Always set a TTL (e.g. 1 hour, or 24 hours), or explicitly invalidate (delete) the cache key (
redis.del(key)) when a user updates their details.
2. Treating Redis as a Permanent Database
Redis stores data in RAM. If the Redis server crashes or restarts, all data is lost. Never use Redis as the only storage for critical data (like users or transactions). Always use a persistent database (PostgreSQL, MongoDB) as the source of truth.
Interview Questions
Why is Redis single-threaded?
Redis uses a single-threaded event loop to process commands sequentially. This simplifies its architecture by avoiding the CPU context-switching overhead of multi-threaded systems. It also eliminates the need for complex data locking mechanisms (concurrency controls), guaranteeing high performance and thread-safe data operations out of the box.
Summary
- Speeds: Operates in RAM, delivering O(1) key lookups in under 1 millisecond.
- Flow: Checks cache first (hit), queries SQL database second (miss), and saves the result in cache (write back).
- Expiry: Always configure TTL parameters to avoid stale records and RAM exhaustion.