MAISON CODE .
/ Tech · Backend · Redis · Caching · Performance · Scale

Redis Caching: The Architecture of Milliseconds

Why your database is the bottleneck. A deep technical guide to Redis patterns (Cache-Aside, Write-Through), Tag Invalidation, and HyperLogLog.

AB
Alex B.
Redis Caching: The Architecture of Milliseconds

In the hierarchy of latency, the network is slow, the disk is sluggish, but RAM is instantaneous.

  • PostgreSQL Query (SSD): 10ms - 100ms.
  • Redis Query (RAM): 0.2ms.

This 500x speed difference is the only reason large-scale web applications stay online. If Amazon hit their SQL database for every product view on Prime Day, the entire internet would crash.

At Maison Code Paris, we treat Redis not just as a “Cache”, but as a primary data structure server. It is the tactical layer that protects the strategic layer (the Database).

Why Maison Code Discusses This

At Maison Code Paris, we act as the architectural conscience for our clients. We often inherit “modern” stacks that were built without a foundational understanding of scale. We see simple APIs that take 4 seconds to respond because of N+1 query problems, and “Microservices” that cost $5,000/month in idle cloud fees.

We discuss this topic because it represents a critical pivot point in engineering maturity. Implementing this correctly differentiates a fragile MVP from a resilient, enterprise-grade platform that can handle Black Friday traffic without breaking a sweat.

The Patterns: How to Cache Correctly

Caching is easy. Invalidating the cache is impossible. There are two main implementation patterns we use.

1. Cache-Aside (Lazy Loading)

This is the default. The application is responsible for reading/writing.

// db/repo.ts
import { redis } from './redis';

async function getProduct(id: string) {
  const key = `product:${id}`;
  
  // 1. Check Cache
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);
  
  // 2. Cache Miss -> Check DB
  const data = await db.product.findUnique({ id });
  
  // 3. Populate Cache
  if (data) {
     // Cache for 60 seconds (TTL)
     await redis.set(key, JSON.stringify(data), 'EX', 60);
  }
  
  return data;
}
  • Pros: Resilient. If Redis dies, the app works (slowly).
  • Cons: Stale Data window. User might see old price for 60s.

2. Write-Through (Event-Driven Invalidation)

We prefer this for High-Consistency data (like Inventory). We listen to database changes (CDC or Application Events) and nuke the cache.

// services/product.ts
async function updatePrice(id: string, price: number) {
  // 1. Update DB (Source of Truth)
  await db.product.update({ where: { id }, data: { price } });
  
  // 2. Invalidate Cache Immediately
  await redis.del(`product:${id}`);
}

The Hardest Problem: Invalidation by Relation (Tags)

Imagine you cache:

  1. product:123
  2. category:shoes (Contains product 123)

If you update Product 123, you delete product:123. But category:shoes still contains the old version of the product! You must also know which categories contain key 123.

We implement Tag-Based Invalidation using Redis Sets.

  1. When creating category:shoes, we track dependencies: SADD tags:product:123 category:shoes
  2. When updating Product 123:
    • Find all keys dependent on it: SMEMBERS tags:product:123 -> Returns ['category:shoes'].
    • Delete them: DEL category:shoes.
    • Clean up the tag set.

This logic is complex but solves the “Why is the homepage still showing the old image?” bug forever.

Serialization: The Hidden CPU Cost

Storing JSON strings (JSON.stringify) is standard but inefficient.

  1. Storage: JSON is verbose. "active": true takes 15 bytes.
  2. CPU: JSON.parse blocks the event loop. For a 1MB payload (large homepage), parsing takes 20ms of CPU time.

Solution: MessagePack (msgpack). It is a binary serialization format. It is faster and smaller.

import { pack, unpack } from 'msgpackr';
import Redis from 'ioredis';

const redis = new Redis({
  // ioredis supports binary buffers
  keyPrefix: 'v1:',
});

await redis.set('key', pack(largeObject)); // Stores Buffer
const buffer = await redis.getBuffer('key');
const obj = unpack(buffer);

Benchmarks show msgpack is 3x faster to encode/decode than native JSON for large structures.

Advanced Structures: Not just Keys and Values

Redis is a “Data Structure Server”. Stop using it like a Map<String, String>.

1. HyperLogLog (Approximate Counting)

Problem: “Count unique visitors today.” Naive: Store every IP in a Set. SADD {ip}. For 100M users, this takes 1.5GB of RAM. Redis Solution: PFADD visitors {ip}. HyperLogLog uses probabilistic math (hashes). It counts 100M unique items with 12KB of RAM. Reliability is 99.19%. Do you need exact visitor counts? No. You need to know “Is it 10k or 100k?”. HLL is perfect.

2. GeoSpatial Indexes

Problem: “Find stores near me.” Naive: SQL Haversine formula (Slow). Redis Solution: GEOADD stores 2.3522 48.8566 "Paris". GEORADIUS stores 2.35 48.85 10 km returns results in microseconds using Geohash sorting.

3. Rate Limiting (The Token Bucket)

We protect our APIs from DDoS using Redis counters. We use a Lua script (to ensure atomicity) that implements the “Sliding Window” algorithm. INCR key. EXPIRE key 60. If > 100, Reject.

The Thundering Herd Problem

If a cache functionality is vital, its failure is catastrophic. Scenario:

  1. home_page key expires at 12:00:00.
  2. At 12:00:01, 5,000 users request the homepage.
  3. 5,000 requests get a “Cache Miss”.
  4. 5,000 requests hit the Postgres Database simultaneously.
  5. Database output buffer fills up. Database crashes.
  6. Website goes down.

Solution: Cache Locking (The Mutex). When a “Miss” occurs, the code attempts to acquire a Lock (SETNX lock:home_page).

  • Winner: Generates the page. Writes to Cache. Releases Lock.
  • Losers: Wait 100ms. Check Cache again. Result: only 1 query hits the database.

10. Redis Cluster vs Sentinel (High Availability)

Single Node Redis is a single point of failure. Sentinel: Monitors Master. Fails over to Replica. Cluster: Shards data across multiple nodes. For e-commerce, we rarely need Cluster (sharding is complex). One Redis instance can handle 100k ops/sec. We prefer Sentinel. But beware: Client configuration for Sentinel is tricky. redis = new Redis({ sentinels: [{ host: 'sentinel-1', port: 26379 }], name: 'mymaster' }). If you hardcode the master IP, failover won’t work.

11. Lua Scripting: Atomicity is King

You want to “Check balance, Deduct money”. Doing this in Node.js (Get -> Logic -> Set) is a Race Condition. We write Lua Scripts that run inside Redis. Lua scripts are atomic. No other command runs while the script is executing. This guarantees that “Inventory Deduction” is perfectly consistent, even with 500 concurrent shoppers.

13. Smart Cache Warming Strategies

Waiting for the first user to hit a “Miss” and pay the latency penalty is bad UX. We implement Active Cache Warming.

  1. On Deploy: Triggers a script warm-cache.ts.
  2. Logic: Fetches the Top 500 URLs from Google Analytics (via API).
  3. Action: Hits the internal API localhost:3000/api/render?url={top_url}. This forces the server to generate the page and populate Redis before traffic hits. The result is 0ms latency for 80% of users immediately after a deployment.

14. Beyond Key-Value: Redis Stack (Search & JSON)

The new Redis Stack allows you to run SQL-like queries on JSON documents. FT.SEARCH productIdx "@title:Shoes @price:[0 100]" this runs in memory. It is 50x faster than Elasticsearch for small datasets (<1M items). We use this for “Instant Search” features where Algolia is too expensive or too slow.

15. Conclusion

Redis is the most powerful tool in the backend engineer’s utility belt. It bridges the gap between the rigid, slow world of ACID Databases and the chaotic, fast request volume of the modern web.

But it requires discipline. An untamed cache (with no TTLs, no eviction policy, and huge keys) is a memory leak waiting to crash your server.


Redis or Die?

Is your PostgreSQL CPU at 90%?

Hire our Architects.