Backend

Smart Caching with Redis

A badly designed cache is just a new source of stale data and strange bugs. This article shows how to build Redis caching, and for which data, so it genuinely speeds things up without breaking.

  • By TETA Team
  • 8 min read
  • Updated:
Smart Caching with Redis

What to cache with Redis

Redis is an in-memory data store, and reading a key usually takes a fraction of a millisecond, while a heavy SQL query may take hundreds. But speed is not the only criterion; the data must be cacheable.

  • Good candidates: product lists, heavy report results, third-party API responses, site settings, expensive computations.
  • Poor candidates: live stock counts, account balances, anything where a stale read costs money.
  • Rule of thumb: read-to-write ratio; at ten to one or more, caching usually pays off.

Before adding a cache, fix the index and the query first; a cache should not be a coat of paint over a bad query. This also appears in scalable API design.

The cache-aside pattern and its alternatives

In cache-aside the application manages the cache itself: it reads the cache first, and on a miss fetches from the database, stores the result and returns it.

js
// Node.js with node-redis
async function getProduct(id) {
  const key = "product:" + id;
  const hit = await redis.get(key);
  if (hit) return JSON.parse(hit);

  const row = await db.query("SELECT * FROM products WHERE id = $1", [id]);
  if (!row) return null;
  // 10 min base TTL + up to 60 s jitter
  const ttl = 600 + Math.floor(Math.random() * 60);
  await redis.set(key, JSON.stringify(row), { EX: ttl });
  return row;
}
Three common caching patterns
PatternHow it worksRisk
Cache-asideApp fills the cache on a missFirst request is slow
Write-throughEvery write updates the cache tooSlower writes; cache fills with cold data
Write-behindWrite to cache first, batch to the DB laterData loss if Redis fails

Start with cache-aside; it has a simple mental model and a Redis failure only makes things slower rather than losing data.

Colourful abstract illustration of a glowing memory layer between an application and a database speeding up the data flow
A cache pays off when the hit rate is high and invalidation is predictable.

TTL, key naming and memory

TTL and eviction

Every key should have a TTL; otherwise the cache slowly fills memory. The TTL balances freshness against database load: site settings might be an hour, product lists one to five minutes. To stop many keys expiring at the same instant, add a few seconds of random jitter.

To cap memory, set maxmemory with the allkeys-lru policy so Redis evicts the least recently used keys when full.

bash
# redis.conf
maxmemory 512mb
maxmemory-policy allkeys-lru

Naming keys

Use a type:id:version structure such as product:42:v3. The version part lets you invalidate every old key by changing a single number in code. Avoid KEYS * in production because it blocks Redis; use SCAN instead.

Invalidation: the hard part

There are three main strategies and a mix of them usually works best:

  1. Time-based expiry: rely on TTL alone. It is simple and enough when a few minutes of staleness is harmless.
  2. Delete on write: after updating a record, DEL its key so the next request rebuilds it.
  3. Versioned keys: keep a group version number in its own key and bump it to invalidate every key in the group at once.

Preventing cache stampedes

When a hot key expires, hundreds of simultaneous requests miss and all run the same heavy query. The database buckles, responses slow down and the problem feeds itself. The simplest defence is a short lock: only one request rebuilds the cache while the others wait briefly.

js
// SET NX = only if absent; EX = auto-release after 10 s
const got = await redis.set("lock:" + key, "1", { NX: true, EX: 10 });
if (got) {
  try { /* rebuild value and SET it */ }
  finally { await redis.del("lock:" + key); }
} else {
  await new Promise(r => setTimeout(r, 50)); // wait, then re-read cache
}

Complementary defences are the TTL jitter mentioned earlier and refreshing very hot keys shortly before they expire.

Operations: persistence, security and monitoring

  • Persistence: if Redis is only a cache you can disable RDB/AOF; if it also holds queues or counters, keep it on and back it up like a database.
  • Security: never expose Redis directly to the internet; use a password plus a firewall or private network.
  • Monitoring: watch the hit rate (keyspace_hits and keyspace_misses in INFO stats), memory use and evictions. A hit rate well below about 80% usually means poor keys or TTLs.

The official command and eviction docs live at redis.io/docs. For dashboards of these metrics see professional monitoring and logging, and if slowness is hurting your search results and user experience, read page load speed optimisation for the client side.

If your system has slowed under load and you are unsure where to start, our web development and optimisation service includes reviewing the caching layer.

Frequently asked questions

Is Redis better than Memcached for caching?

Both work for simple caching. Redis offers richer data structures (hashes, sets, sorted sets), flexible TTLs and optional persistence, which is why it is the default choice in most projects.

What TTL should I use?

There is no fixed number. It depends on how much staleness the data tolerates: seconds for dynamic data, minutes for lists, hours for settings. Always add a few seconds of random jitter.

What happens if Redis goes down?

With cache-aside the app should fall back to the database and merely get slower. To avoid a full outage, set short timeouts on Redis connections and treat cache errors as misses.

Why is my cache hit rate low?

Usually because of very short TTLs, keys that include changing parameters such as timestamps, or caching data that is rarely read twice. Review your keys and access pattern.

Comments

    No comments yet. Be the first to share your thoughts.

    Comments are published after review.