Back to Blog
Lesson 9 of the Redis: Redis Essentials & Data Types course
DatabasesJuly 27, 20264 min read

Improving Cache Hit Ratios: A Guide to Cache-Aside Logic

Master the cache-aside pattern to improve cache hit ratios. Learn to implement cache-miss logic, populate Redis dynamically, and handle data updates effectively.

RedisCachingNode.jsAPIPerformanceBackend
Artistic shallow focus image of a measuring tape showing numbers and units.

Previously in this course, we covered Building a Simple API Response Cache with Node.js and Redis, where we established the basic flow of checking a key and returning its value. In this lesson, we level up that implementation by focusing on Improving Cache Hit Ratios through the cache-aside pattern—the industry standard for maintaining a performant and consistent data layer.

The Cache-Aside Pattern from First Principles

A "cache hit" happens when your application requests data and finds it in Redis, avoiding a trip to your primary database. A "cache miss" occurs when the data isn't there, forcing the application to fetch it from the source of truth (like PostgreSQL or a third-party API) and then update Redis.

If your cache hit ratio is low, your application suffers from high latency and unnecessary load on your primary database. To solve this, we implement the cache-aside logic:

  1. Check Cache: Attempt to GET the data from Redis.
  2. Handle Miss: If the key is missing, fetch the data from the database.
  3. Populate: SET the result into Redis so the next request hits the cache.
  4. Return: Send the data back to the client.

Implementing Cache-Miss Logic

Let's refine our API project. We’ll create a function that handles the logic cleanly. If you haven't set up your project yet, refer back to Setting Up the Backend Project Baseline with Node.js and Redis.

JAVASCRIPT
const getCachedData = async (key, fetchFunction) => {
  // 1. Check Redis
  const cached = await redis.get(key);
  if (cached) {
    console.log("Cache Hit!");
    return JSON.parse(cached);
  }

  // 2. Cache Miss: Fetch from source
  console.log("Cache Miss. Fetching from DB...");
  const data = await fetchFunction();

  // 3. Populate Redis
  // We use EX to set a TTL, preventing stale data from living forever
  await redis.set(key, JSON.stringify(data), CE9178">'EX', 3600);

  return data;
};

Handling Data Updates (Invalidation)

One of the biggest pitfalls in distributed systems is serving "stale" data—where the database has changed, but the cache still holds the old value. To maintain high hit ratios without sacrificing correctness, you must invalidate or update the cache when the underlying data changes.

When performing a write operation (e.g., UPDATE users SET...), you have two choices:

  • Delete the key: The next read will trigger a cache miss and fetch the fresh data. This is safer and simpler.
  • Update the key: Overwrite the existing Redis key with the new data.
JAVASCRIPT
const updateUser = async (userId, newData) => {
  // 1. Update the database
  await db.updateUser(userId, newData);
  
  // 2. Invalidate the cache(Remove the key)
  // This forces the next request to fetch the fresh, updated data
  await redis.del(CE9178">`user:${userId}`);
};

Hands-on Exercise

Using the project structure we've developed, modify your /api/user/:id endpoint:

  1. Implement the getCachedData wrapper shown above.
  2. Create a "dummy" database fetch function that simulates a 2-second delay.
  3. Observe the logs: The first request should show "Cache Miss," and subsequent requests to the same ID should show "Cache Hit."
  4. Write a simple PUT endpoint that updates a user and calls redis.del to invalidate the cache.

Common Pitfalls

  • Cache Stampede: If a popular key expires, multiple concurrent requests might see a cache miss and all try to fetch from the DB at once. Use locking or probabilistic early expiration to mitigate this.
  • Infinite TTLs: Never set a key without an expiration unless you have a robust invalidation strategy. You will eventually run out of memory.
  • Serialization Errors: Always ensure your JSON.parse is wrapped in a try/catch block, as malformed data in Redis can crash your node process.

FAQ

Q: Should I cache everything? A: No. Only cache data that is "read-heavy" and relatively static. Caching frequently changing data leads to high invalidation overhead.

Q: What is the ideal cache hit ratio? A: It depends on your workload, but 80%–95% is a healthy target for most API response caches.

Q: Is redis.del slow? A: DEL is an O(1) operation for most keys and is extremely fast. It is the preferred way to handle data updates.

Recap

Improving your cache hit ratio relies on a disciplined cache-aside strategy. By implementing graceful cache-miss handling and proactive cache invalidation (like we discussed in Managing Cache via Dashboard and API: A Developer's Guide), you drastically reduce backend latency and database pressure. Remember: it is better to have a cache miss than to serve stale, incorrect data to your users.

Up next: We'll dive into Managing Hash Operations, which allow us to store structured objects in Redis more efficiently than simple strings.

Similar Posts