DEV Community

Anas Sheikh
Anas Sheikh

Posted on

Why revalidateTag() Does Absolutely Nothing for the Data You Just Changed

This one causes a specific kind of confusion because every piece of the code looks correct in isolation, you tagged a fetch, you called revalidateTag with that exact same tag string after a mutation, and the page keeps showing the old data anyway, like the cache invalidation call did nothing at all.

The Setup That Looks Right

// lib/products.ts
export async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { tags: ['products'] },
  });
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode
// actions/products.ts
'use server';
import { revalidateTag } from 'next/cache';

export async function updateProduct(id: string, data: ProductUpdate) {
  await db.products.update({ where: { id }, data });
  revalidateTag('products'); // should invalidate the tagged fetch above
}
Enter fullscreen mode Exit fullscreen mode

This is exactly the documented pattern, and it works correctly for this specific case. The confusion starts the moment a second, very common pattern gets mixed into the same codebase.

Where It Actually Breaks

// lib/products.ts
export async function getProducts() {
  // Direct database call, no fetch involved at all
  return db.products.findMany();
}
Enter fullscreen mode Exit fullscreen mode
'use server';
import { revalidateTag } from 'next/cache';

export async function updateProduct(id: string, data: ProductUpdate) {
  await db.products.update({ where: { id }, data });
  revalidateTag('products'); // this tag was never attached to anything
}
Enter fullscreen mode Exit fullscreen mode

revalidateTag only invalidates cache entries created by Next.js's fetch caching layer, specifically entries that were tagged through the next: { tags: [...] } option on a fetch call. A direct database query through Prisma, Mongoose, or any ORM never goes through that caching layer in the first place, so there's no tagged cache entry for revalidateTag('products') to actually invalidate. The call runs without error, returns successfully, and does precisely nothing, because the thing it's supposed to invalidate was never created.

Why This Is So Easy to Miss

The mental model most people build around caching in Next.js is "tag your data, then revalidate the tag when it changes," which is correct, but it specifically describes fetch-based caching, not a general-purpose tagging system that applies to any way you happen to load data. A codebase that uses fetch for external API calls and a direct ORM call for its own database, which is an extremely common and reasonable setup, ends up with revalidateTag working perfectly for one data source and silently doing nothing for the other, with identical-looking code calling it in both places.

Making this worse, unstable_cache, the function meant for wrapping non-fetch data sources like direct database calls, also supports tags, and looks almost identical to use:

import { unstable_cache } from 'next/cache';

// This one DOES respond to revalidateTag('products'),
// because unstable_cache wraps the function in Next's cache layer directly
export const getProducts = unstable_cache(
  async () => db.products.findMany(),
  ['products'],
  { tags: ['products'] }
);
Enter fullscreen mode Exit fullscreen mode

The difference between this working version and the broken one from earlier is a wrapping function most people don't realize they needed, not anything about the tag string itself.

The Actual Rule

revalidateTag only invalidates cache entries that were created through Next's caching layer, either a tagged fetch call or a tagged unstable_cache wrapper. A raw database or ORM call with no caching wrapper around it was never cached by Next.js in the first place, so there's nothing for the tag to reach. If you're seeing stale data survive a revalidateTag call that you've triple-checked uses the matching tag string, the actual question isn't whether the tag matches, it's whether the data was ever going through Next's cache at all.

How to Actually Check

Go look at wherever your app fetches the data a revalidateTag call is supposed to refresh. If it's a direct ORM or database call with no unstable_cache wrapper, that's the gap, not a typo in your tag name, not a timing issue, not a bug in Next.js itself. Wrap it in unstable_cache with the matching tag, or restructure the fetch to actually go through the fetch caching layer, and the exact same revalidateTag call that was previously doing nothing starts working immediately.

This caching layer distinction is something I ended up documenting pretty thoroughly while building out the data layer for my Next.js SaaS templates, specifically because mixing direct MongoDB calls with fetch-based external API calls in the same project makes this gap easy to hit without noticing.

Get the templates: https://pixelanas.gumroad.com

Check your own revalidateTag calls right now, specifically whether the data behind them actually goes through a tagged fetch or unstable_cache, or just a raw database call that was never cached to begin with. Drop what you find in the comments.


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751

Top comments (0)