BlogCache Components

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

app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'

export async function revalidatePrice(productId: string) {
  revalidateTag(`product-price-${productId}`, 'max')
  //                                            ↑
  //                                    Revalidation Profile
}

Behavior

  1. Marks the cache as "stale"
  2. Continues serving old content while regenerating in the background
  3. 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

app/actions.ts
'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 }
}

Direct Comparison

AspectrevalidateTagupdateTag
BehaviorStale-while-revalidateImmediate Invalidation
LatencyLow (serves old first)Medium (regenerates on request)
ConsistencyEventualImmediate
PerformanceBetterGood
Use caseEditorial contentUser mutations
Second argumentYes (profile)No

Use Case Examples

Blog / Editorial Content

app/actions.ts
"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

app/actions.ts
"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

app/actions.ts
"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

app/actions.ts
"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:

app/actions.ts
"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

app/actions.ts
"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

  1. Click "Revalidate"
  2. Refresh the page
  3. View Source (Ctrl+U)
  4. 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 | STALE
  • HIT - Content from cache
  • MISS - Regenerated
  • STALE - 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);
// 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.

On this page