Last update
Next.js 16 offers two ways to invalidate cache: revalidateTag and updateTag. Both work with tags but have different behaviors.
updateTag vs. revalidateTag
Stale-while-revalidate
'use server'
import { revalidateTag } from 'next/cache'
export async function revalidatePrice(productId: string) {
revalidateTag(`product-price-${productId}`, 'max')
// ↑
// Revalidation Profile
}Behavior
- Marks the cache as "stale"
- Continues serving old content while regenerating in the background
- The next request gets the new content
Timeline
t=0s User 1 clicks "Revalidate"
→ Cache marked as stale
→ Continues serving old version
t=1s User 2 visits the page
→ Receives old version
→ Triggers background regeneration
t=3s Regeneration complete
t=5s User 3 visits the page
→ Receives NEW version ✨Available Profiles
revalidateTag('my-tag', 'max') // Most aggressive
revalidateTag('my-tag', 'default') // Balanced
revalidateTag('my-tag', 'min') // Most conservative
// Or custom
revalidateTag('my-tag', {
stale: 3600,
revalidate: 7200,
expire: 86400
})Use when
✅ Immediate change is not critical
✅ You can tolerate old data briefly
✅ Blog posts, prices, editorial content
✅ You want to minimize latency for users
Real Example
'use server'
import { revalidateTag } from 'next/cache'
// Update product price
export async function updateProductPrice(
productId: string,
newPrice: number
) {
// 1. Update in DB
await db.query(
'UPDATE products SET price = ? WHERE id = ?',
[newPrice, productId]
)
// 2. Revalidate cache (stale-while-revalidate)
revalidateTag(`product-price-${productId}`, 'max')
return { success: true }
}Immediate Invalidation
'use server'
import { updateTag } from 'next/cache'
export async function updateCartItem(userId: string) {
updateTag(`user-cart-${userId}`)
// Second argument not required
}Behavior
- Expires the cache immediately
- The next request regenerates the content
- User gets fresh content in the same request
Timeline
t=0s User 1 clicks "Add to cart"
→ Cache EXPIRES immediately
t=1s User 2 visits the page
→ Cache expired, regenerates on request
→ Receives NEW version ✨
t=2s User 3 visits the page
→ Receives NEW version ✨Use when
✅ Change must be immediate
✅ You cannot tolerate old data
✅ Shopping cart
✅ Likes, votes, reactions
✅ User mutations
Real Example
'use server'
import { updateTag } from 'next/cache'
// Add item to cart
export async function addToCart(
userId: string,
productId: string
) {
// 1. Add to DB
await db.query(
'INSERT INTO cart_items (user_id, product_id) VALUES (?, ?)',
[userId, productId]
)
// 2. Invalidate cache immediately
updateTag(`user-cart-${userId}`)
return { success: true }
}Direct Comparison
| Aspect | revalidateTag | updateTag |
|---|---|---|
| Behavior | Stale-while-revalidate | Immediate Invalidation |
| Latency | Low (serves old first) | Medium (regenerates on request) |
| Consistency | Eventual | Immediate |
| Performance | Better | Good |
| Use case | Editorial content | User mutations |
| Second argument | Yes (profile) | No |
Use Case Examples
Blog / Editorial Content
"use server";
import { revalidateTag } from "next/cache";
export async function publishBlogPost(postId: string) {
await db.query("UPDATE posts SET published = true WHERE id = ?", [postId]);
// Stale-while-revalidate: OK that some see old version
revalidateTag(`blog-post-${postId}`, "max");
revalidateTag("blog-list", "max");
}E-commerce / Cart
"use server";
import { updateTag } from "next/cache";
export async function addToCart(userId: string, productId: string) {
await db.query("INSERT INTO cart_items (user_id, product_id) VALUES (?, ?)", [
userId,
productId,
]);
// Immediate invalidation: user must see their cart updated NOW
updateTag(`user-cart-${userId}`);
}Likes System
"use server";
import { updateTag } from "next/cache";
export async function likePost(userId: string, postId: string) {
await db.query("INSERT INTO likes (user_id, post_id) VALUES (?, ?)", [
userId,
postId,
]);
// Immediate invalidation: user must see their like reflected
updateTag(`post-likes-${postId}`);
updateTag(`user-liked-posts-${userId}`);
}Product Prices
"use server";
import { revalidateTag } from "next/cache";
export async function updatePrice(productId: string, newPrice: number) {
await db.query("UPDATE products SET price = ? WHERE id = ?", [
newPrice,
productId,
]);
// Stale-while-revalidate: OK that the price takes a bit to update
revalidateTag(`product-price-${productId}`, "max");
}Revalidating Multiple Tags
Both functions can be called multiple times:
"use server";
import { revalidateTag, updateTag } from "next/cache";
export async function updateProduct(productId: string, data: ProductData) {
await db.query("UPDATE products SET ... WHERE id = ?", [productId]);
// Mix both approaches as needed
revalidateTag(`product-text-${productId}`, "max"); // Text: stale-while-revalidate
revalidateTag(`product-price-${productId}`, "max"); // Price: stale-while-revalidate
updateTag(`product-inventory-${productId}`); // Inventory: immediate
}revalidatePath vs. Tags
revalidatePath (traditional)
revalidatePath("/product/1");Invalidates: The whole route /product/1
Problem: You cannot invalidate specific fields
Tags (granular)
revalidateTag("product-price-1", "max");Invalidates: Only the price cache of product 1
Advantage: Surgical, does not affect other fields
General rule: Use tags for granular control. Only use revalidatePath
when you want to invalidate an entire page.
Debugging Revalidation
Useful Logs
"use server";
import { revalidateTag } from "next/cache";
export async function revalidatePrice(productId: string) {
console.log(
`[Revalidation] product-price-${productId} at ${new Date().toISOString()}`
);
revalidateTag(`product-price-${productId}`, "max");
}Verify in Browser
- Click "Revalidate"
- Refresh the page
- View Source (Ctrl+U)
- Look for updated content in the HTML
If the content changed, revalidation worked.
Response Headers
Next.js includes useful headers:
x-nextjs-cache: HIT | MISS | STALEHIT- Content from cacheMISS- RegeneratedSTALE- Stale-while-revalidate in progress
Best Practices
1. Use the Correct Approach
// ✅ Editorial content
revalidateTag("blog-post", "max");
// ✅ User mutations
updateTag("user-cart");2. Descriptive Tags
// ✅ Good
cacheTag(`product-price-${productId}`);
cacheTag(`user-profile-${userId}`);
// ❌ Bad
cacheTag("data", id);
cacheTag("cache1", value);3. Revalidate Related Tags
// Update product → revalidate multiple tags
revalidateTag(`product-${productId}`, "max");
revalidateTag("product-list", "max");
revalidateTag(`category-${categoryId}`, "max");4. Batch Operations
// If you update multiple products, group them
export async function updateMultipleProducts(productIds: string[]) {
await db.transaction(async (tx) => {
// Updates in transaction
});
// Revalidate all together
productIds.forEach((id) => {
revalidateTag(`product-${id}`, "max");
});
}Performance: Too many simultaneous revalidations can cause load. Use sparingly.