Next.js Discord

Discord Forum

Cache Components revalidation differs between local build and Vercel when sharing the same data func

Answered
Nuitari posted this in #help-forum
Open in Discord
I'm seeing different cache invalidation behavior between local builds and Vercel deployments when using Cache Components with use cache and cacheTag in Next.js 16.1.
Answered by andrei
It looks like this issue is caused by how Vercel’s Data Cache behaves with use cache compared to the local runtime.

On Vercel, the default cache layer is global (Data Cache) and tag invalidation appears to be coarser than expected, causing multiple tagged entries for the same logical entity (product id) to be invalidated together.

A workaround that fixes the issue is forcing the cache to be remote-scoped explicitly:
"use cache: remote"

instead of:
"use cache"

Important you need to cache the component not the request to db:

export async function ProductRating({ id }: { id: number }) {
  "use cache: remote";
  cacheTag(`rating-${id}`);
  cacheLife("nuncaSeRevalida")
  const data = await getProductField2({id, field:"rating"});
  return (
      <StarRating rating={data } /> 
  );
}
View full answer

152 Replies

Architecture

- App Router
- Multiple Suspense boundaries inside a product card
- Each section is a cached component with its own cacheTag

Example:

- ProductDescriptioncacheTag("description-id")
- ProductPricecacheTag("price-id")
- ProductMetadatacacheTag("metadata-id")

Each component is rendered inside its own Suspense boundary.

Cached component example

export async function ProductDescription({ id }: { id: number }) {
  "use cache";
  cacheTag(`description-${id}`);

  const productData = await getProduct(id);

  if (!productData) return null;

  return <p>{productData.description}</p>;
}


Other cached components also call the same function:

const productData = await getProduct(id)


but use different cacheTags.

Observed behavior

Local (dev + local production build)

- Invalidating a specific tag using updateTag
- Only the related cached component re-renders
- Other components remain cached as expected

Vercel deployment

- If multiple cached components call the same data function / promise
- Invalidating one tag appears to cause all components using that function to refresh

So effectively the cache invalidation behaves as if the components share a dependency through the same async function.

Question

Is there any difference in how Cache Components + tag invalidation interact with the Vercel Data Cache compared to local builds?

Or could the shared async function / promise deduplication cause the cache dependency to be shared in production?

Environment

Next.js: 16.1
Deployment: Vercel
Runtime: Bun
local
vercel
sometimes in local build i also get full revalidation, to detect rerender i move update date to ddbb and compare it with a margin of few seconds. Also my cliente form doesnt forget previous dirty fields still need to work on it but i appreciate if someone can confirm wich is the intended behaviour
So the issue is locally only the tag that is updateTag() is invalidated, but hosted on vercel, all tags realated to the one tags value are invalidated?
@Nuitari
i swear it was working fine but suddenly it have same behaviour that why i try to make a demo
i try many options first was a shared promise and unwrap it inside each suspense with cache only on componentes then split request using same function caching the request to bbdd, then split each field request in one call to db.
i remove date and switch to store last update in bbdd because i had issues using Date
also those components have kinda same code , category (badge) in the left and brand in the right. if i disable js some of them appear to be streamed and not part of the shell
but if you hover them the time since last render is the same
So the problem is unrelated data is being invalidated when you use update tag? But the issue only happens when hosted on vercel?
Yes, the local build works as intended. Only Vercel seems to update all the data of the edited product. I'm also updating the parallel routing, by the way.
And the way we can tell other tags are being invalidated is because of the date stamp that we're using?
I would say then first look into the way the times being invalidated and check that out
yes
Like do we pass the time from the back end when the data is updated or do we get the time from the front end??
i have try to change wait of get data few times first was a shared promise
So that shared promise comes from the server and gets past to a child component
i tried to use a timer dont work to me
Where does the time come from? Does it come from the server or does it come from the front end?
so i ended added the updatedat in the database
so when component is cached that value is a prop
pass to client
there if time is lower than 10s blik and you see the diff of time in the tooltip
local build i just change rating
price 3h
that is impossilbe in vercel
Oh you can't get the page to show static because you have to decode the params which would make it dynamic. I get it now
i have one param but is the intercepting routing (the sidebar)
Any of those components that are cached remove the suspense. Keep the component but remove the suspense
The suspense will cause a stutter to the screen if the data is cached. It's known at build time and is part of the static shell
Suspense is not needed with cached data because it's known at build time and then revalidated later
We don't want to show fallbacks if we already have the data and having suspense with cache data causes a stutter of the fallback because it tries to show it even though it doesn't need to
and i still chack update each part '
Yes, you should still have granular control. I believe it is your suspense that's causing the issues
But think about it from a ux perspective. The server has the data the server sending the data. Why should the user see a fallback before the data if the data is known at build time?
It should just show up with the rest of the page
The whole product card would probably have to be wrapped in suspense so that you can access the dynamic params, but after the initial load it should be cached across deployments
Lol
local build for now work fine
same granular cache
I have another question, how frequently is this data changing? Could we not just fetch the data all in one promise and then just invalidate that when the data updates?
also now shell is properly rendered
I mean if we're doing five data fetches for five components, could we not just do one data fetch for all the data? Like do we really need the granule control?
right now im getting each data in one query
Are we not doing data fetches inside of the child components? It looks like the product ID comes from the card and then passes the ID to all the smaller components to fetch the data
export async function getProductField<
  K extends keyof typeof products & keyof Product,
>(id: number, field: K) {
  "use cache";
  cacheTag(`${field}-${id}`);

  const col = products[field];
  if (!(col instanceof Column)) return null;

  const productslist = await db
    .select({ value: col, updatedAt: products.updatedAt })
    .from(products)
    .where(eq(products.id, id));

  const product = productslist[0];
  if (!product) return null;

  const rawDate = (product as { updatedAt?: string }).updatedAt ?? "";
  const date = new Date(
    rawDate.endsWith("Z") ? rawDate : `${rawDate} Z`,
  ).getTime();

  return {
    [field]: product.value,
    date,
  } as { [P in K]: Product[P] } & { date: number };
}
Could we not just fetch all the data at once on the page?? We would lose granule control but we could just invalidate the whole data fetch every time. Any part of the data changes
Like is it important that we have fresh product name but a not fresh rating?
gonna try in vercel i dont know why i had the idea cached parts needed to be wrapper in suspense too
Because that's how promises work. You pass a promise to a component. You wrap it in suspense
i rly dont mind about fetching data
im more about not rendering components
But what I think is happening is you're decoding the param and then when the param is decoded the product card is wrapped in suspense which is causing all the other data fetches to fire
im not using param except in router interception
Because checking a param is dynamic, so the product card the whole entire card would still have to be wrapped in suspense, which I think is causing your other components to fetch data because the component is being remounted
Yes, if you use params in the URL and you check them on the page, the page becomes dynamic
If you check them in a child component that child component becomes dynamic and needs to be wrapped in suspense, which I think is causing all the other children to revalidate
the left part is the id and it have generate static params revalidate all data that work fine
Hmmm
First things first, remove the suspenses and see if that improves
i did already im deploying to vercel
Rad
keep updating evrything
Hmm can I have the repo I'd liked to look at the code if it's public
Ty I'm just about to shower then I'll take a look
dw im not in hurry
it just a wanted to understand how to achieve that granularity
wanst sure the best approach to reduce db calls
Me to do if this ever happens to me I can figure it out
Lol well fetch all product data in one fetch is an easy way to reduce connections
But I do understand the need to want granularity
exactly but for some reason i though i needed suspense XDDD
It's just in my mind if we have all this product information why not send it in one fetch and then when we revalidate the information all the information is sent in one fetch. That way we save five database calls
It's just the thought pattern you think that you're fetching data and that's dynamic so that it should be wrapped in suspense
so i need to pass a promise to get data XDDD inside suspense
Cache components is new for everybody
With cache components you can cache components but you can also cache data fetches
y in fact i have both in this test i need to test best combinations of boths but before wanted to know why build behave differently thx for help when you have a time and find any reason let me know
And you might not need suspense, but if you're accessing prams then you would need suspense
i used turso as ddbb
Kk
thx for your help when you have time take a loot.
can i make you a fast unrelated question?
I might try to run it on vercel, if you had a copy of the DB I could use the key for that would be great so that I have the same structure but no access to your actual DB
DM me
If it's unrelated to this post
sure il pass you my env is about paralel routing now it have activity on it
Oh is it too hard to spin up a dummy instance??
I don't want your actual database key. I want a key for a database that's a copy of yours. Not necessarily even with data. Just structure wise
So I can build the project and run it on Vercel
is not
Rad
you have seed script
use drizzle
build should create it
Ok rad np 🙂 iv never used Frizzell but I like to learn
Drizzel
my first time with it kinda damm easy at least with bun
Love bun
Cache components doesn't play nice with bun --bun yet but their getting there
Using bun as a runtime for cache components. Not great using it as a package manager and a runner. Pretty good
ok but since local seem to work
il try to make one with pnpm just to be sure is not bun
Are you using bun --bun dev? In your script ?
100%. That's why it's not working right on Vercel if you are
yes
Lol
no sorry i use bun dev
Ohhh bun dev run it on node
Bun --bun run dev runs it on bun runtime
same with start?
If you do it like the picture I just sent that's using the bun run time instead of node
Cache components I've found does not play well on the bun runtime currently
ok i run the build with --bun but seem is the same caching granular
Bun is a package manager and a runtime and more where node has node npm and so on bun just has bun
@Nuitari ok i run the build with --bun but seem is the same caching granular
Do you ever get a warning about set timeout or cash components not working properly when you run a build?
Because when I use those bun commands in front of run Dev build and start commands and cache components. I have issues
i dont notice any warning
Hmmm
Try using the node runtime and see if you still have the issue on Vercel
i was using node without knowing i was xD
Hmmmm
Lol I want to figure this out lol
is kinda empty project
Honestly, I think your approach is kind of smart as well though because I don't have a timestamp on anything. So I really don't know if I have granular control when I do this LOL
shadcn drizzle and that all
is a prototype i saw this in vercel demos so i try to replicate it i have ppl complaing about next evryday i need weapons to fight ignorance xD
Lol
And you've ran into validation issues lol. Seems like your arming the other team
xDDD no because honesty mean you dont fake data. You have no idea how much problems are having people trying to replicate Nextjs features in astro
one friend got free plan almost expired on astro ISR
anyway i wont bother you il make new test and approach if you find out let me know gonna share news about dont need suspense
XD
Rad and I don't mind conversations at all. I'm literally here to fill time lol
Vercel has a great hobby plan and I've never paid
But I also only have like five users
I did upgrade to pro so I guess I 20 a month but I do that for features not limits
It looks like this issue is caused by how Vercel’s Data Cache behaves with use cache compared to the local runtime.

On Vercel, the default cache layer is global (Data Cache) and tag invalidation appears to be coarser than expected, causing multiple tagged entries for the same logical entity (product id) to be invalidated together.

A workaround that fixes the issue is forcing the cache to be remote-scoped explicitly:
"use cache: remote"

instead of:
"use cache"

Important you need to cache the component not the request to db:

export async function ProductRating({ id }: { id: number }) {
  "use cache: remote";
  cacheTag(`rating-${id}`);
  cacheLife("nuncaSeRevalida")
  const data = await getProductField2({id, field:"rating"});
  return (
      <StarRating rating={data } /> 
  );
}
Answer