3 min read

Next.js Cache Invalidation in Practice: Redis Tag Cache and Revalidation Strategies

Practical design guide to simultaneously secure cache hit rate and data freshness by using Redis tag cache in Next.js App Router

Next.js Cache Invalidation in Practice: Redis Tag Cache and Revalidation Strategies thumbnail

Introduction

As traffic increases, most Next.js services repeat the same problem. “If you use a strong cache for response speed, data will be reflected late, and if you prioritize freshness, each request becomes expensive.” In particular, since page/data/route caches are mixed and operated based on App Router, it is difficult to solve the entire problem with a simple TTL.

This article covers a practical pattern that uses Next.js revalidation API (revalidateTag, revalidatePath) together with Redis tag cache to achieve both cache hit rate and data freshness.

Problem definition

Failure patterns that frequently occur in an operating environment are as follows.

  • Only some pages display old data after content modification
  • Multiple pages refer to the same entity, making it difficult to narrow down the scope of invalidation.
  • Instantaneous load spikes occur with the “delete entire cache” method
  • Cache key rules differ for each code, increasing debugging difficulty

The key is to change the "invalidation unit" to a tag-based domain event rather than a key.

Concept explanation

A separate view of the cache hierarchy

tierPurposerepresentative toolsInvalidation method
Next.js data/route cacheSSR/ISR cost savingsfetch cache, App Router cacherevalidateTag, revalidatePath
application cacheDomain query reuseRedisDelete key/tag mapping
CDN/Edge CacheMinimize global response delayVercel Edge/CDNHeader/redistribution based invalidation

In practice, these three layers are not erased at once. The safe order is to first clear the application cache based on the "domain tag" and then revalidate the Next cache that references that tag.

Tag design principles

  1. Entity unit tags: post:123, author:42
  2. List unit tags: post:list, post:tag:nextjs
  3. Derived view tags: home:featured, sidebar:popular

If you divide it like this, you can accurately invalidate only the required range when “editing one article.”

Code example

Example 1: Redis tag cache save/delete utility

import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);

function dataKey(key: string) {
  return `cache:data:${key}`;
}

function tagSetKey(tag: string) {
  return `cache:tag:${tag}`;
}

export async function setWithTags<T>(
  key: string,
  value: T,
  tags: string[],
  ttlSeconds: number,
) {
  const serialized = JSON.stringify(value);
  const dKey = dataKey(key);

  const tx = redis.multi();
  tx.set(dKey, serialized, "EX", ttlSeconds);

  for (const tag of tags) {
    tx.sadd(tagSetKey(tag), dKey);
    tx.expire(tagSetKey(tag), ttlSeconds);
  }

  await tx.exec();
}

export async function invalidateByTag(tag: string) {
  const setKey = tagSetKey(tag);
  const members = await redis.smembers(setKey);

  if (members.length === 0) {
    return 0;
  }

  const tx = redis.multi();
  tx.del(...members);
  tx.del(setKey);
  await tx.exec();

  return members.length;
}

The point is to “reverse index the data key into the tag set”. In this way, only the relevant cache can be removed with a single domain event.

Example 2: Chaining from a change event to Next revalidation

import { revalidatePath, revalidateTag } from "next/cache";
import { invalidateByTag } from "@/lib/cache/tag-cache";

export async function onPostUpdated(postId: string, slug: string) {
  const tags = [
    `post:${postId}`,
    "post:list",
    "home:featured",
  ];

  // 1) 애플리케이션 캐시 무효화
  await Promise.all(tags.map((tag) => invalidateByTag(tag)));

  // 2) Next.js 캐시 재검증
  tags.forEach((tag) => revalidateTag(tag));

  // 3) 경로 단위 재검증 (fallback 안전장치)
  revalidatePath(`/posts/${slug}`);
  revalidatePath("/");
  revalidatePath("/posts");
}

Rather than viewing revalidateTag and revalidatePath as competition, it is more advantageous to respond to failures by operating them as a combination of tag priority + path fallback.

Architecture Description

Mermaid diagram rendering...

The key is to bundle "modification event -> tag invalidation -> revalidation -> reload" to behave as one transaction flow.

Trade-off analysis

SelectAdvantagesDisadvantagesRecommended Situation
TTL-centric simple cacheVery simple implementationDifficulty ensuring up-to-datenessLow change frequency content
Tag-based precision invalidationExcellent freshness/performance balanceTag design cost requiredOperational service
Clear entire cacheGet consistency right awayInstantaneous load surgeTemporary in response to emergency failure

The cost of the tag approach is concentrated on “initial modeling”. However, once you set a standard, you don't have to reinvent the cache policy every time you add a feature.

Cleanup

The key to Next.js cache management is not to keep a lot of cache, but to clear it accurately. To apply it immediately in practice, start with the steps below.

  1. Fix the domain tag naming rules first.
  2. Implement Redis tag inverse index.
  3. Standardize the invalidateByTag -> revalidateTag -> revalidatePath order in change events.
  4. Observe cache hit rate and revalidation delay time as metrics.

By maintaining this structure, you can operate closer to “fast and up-to-date response” even when traffic increases.

Read together

Comments