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
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
| tier | Purpose | representative tools | Invalidation method |
|---|---|---|---|
| Next.js data/route cache | SSR/ISR cost savings | fetch cache, App Router cache | revalidateTag, revalidatePath |
| application cache | Domain query reuse | Redis | Delete key/tag mapping |
| CDN/Edge Cache | Minimize global response delay | Vercel Edge/CDN | Header/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
- Entity unit tags:
post:123,author:42 - List unit tags:
post:list,post:tag:nextjs - 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
The key is to bundle "modification event -> tag invalidation -> revalidation -> reload" to behave as one transaction flow.
Trade-off analysis
| Select | Advantages | Disadvantages | Recommended Situation |
|---|---|---|---|
| TTL-centric simple cache | Very simple implementation | Difficulty ensuring up-to-dateness | Low change frequency content |
| Tag-based precision invalidation | Excellent freshness/performance balance | Tag design cost required | Operational service |
| Clear entire cache | Get consistency right away | Instantaneous load surge | Temporary 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.
- Fix the domain tag naming rules first.
- Implement Redis tag inverse index.
- Standardize the
invalidateByTag -> revalidateTag -> revalidatePathorder in change events. - 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.