Next.js Discord

Discord Forum

Time based fetch-cache revalidation 15.5.15

Unanswered
Goldstripe sardinella posted this in #help-forum
Open in Discord
Goldstripe sardinellaOP
Hey, I’m debugging a Next.js 15 App Router caching issue and want to check what I’m missing.

We have a dynamic ad page that fetches ad data from an external API with time-based revalidation:
(simplified fetch)
await fetch(url, {
cache: 'force-cache',
next: { revalidate: 60 }, // normally 15 min, 60s for testing
});

The page also has:

export function generateStaticParams() {
return [];
}

Flow:

User visits an ad page while the ad exists.
API returns 200 with JSON data.
Next creates a .next/cache/fetch-cache entry with the correct URL, status: 200, revalidate: 60, etc.
Later the ad is removed from the API.
The same API endpoint now returns 204 No Content with an empty body.
In our fetch wrapper, 204 returns null.
In page.tsx, null calls notFound().

Expected: after the revalidate window, background revalidation gets the 204, updates/invalidates the old fetch-cache entry, and the page eventually renders as notFound() / 404.

Actual: the old cached 200 response seems to remain in the Data Cache. The page keeps rendering old ad data, and the fetch-cache entry still has status: 200.

If I bypass the cache, the API correctly returns 204 and the page calls notFound(), so the page logic works.

Is this expected with time-based revalidation? Does 204 No Content count as failed/non-cacheable background revalidation, causing Next to keep the previous cached 200 response? What’s the best approach if we still want to use time-based revalidation here?

7 Replies

Goldstripe sardinellaOP
I also tested deleting the fetch-cache entry for one specific ad. After rebuilding and restarting the app, that ad correctly returned 404. However, other ad IDs that still have old fetch-cache entries continue to render the stale cached data instead of returning 404, even though the API now returns 204 for them.
Goldstripe sardinellaOP
- API response status 204 wasn't causing this (tested with 404 as well)
- next-navigation notFound(); when data is null doesn't cause this (switched notFound() to instead just return h1 text "Not found")
Goldstripe sardinellaOP
Switching the API response status (with empty response) to 200 - it (obviously) then updated the cached data - and 404 page is displayed. So there's something that Im missing how to handle the time based fetch data invalidation when response.status != 200 😄
Spectacled bear
I’d treat this as a stale-while-revalidate edge case rather than relying on the 204/404 to “discover” deletion.

With time-based revalidation, if the background revalidation path errors/throws, Next keeps serving the last successful cached result and retries later. An empty/non-200 response can easily end up there depending on the wrapper, especially if anything tries to parse an empty body or throws before returning null.

For deleted ads I’d usually do one of these:

1. tag the fetch, e.g. next: { revalidate: 900, tags: [\ad:${id}`] }, then callrevalidateTagorrevalidatePathwhen the ad is deleted 2. return a cacheable 200 “tombstone” payload like{ deleted: true }, then rendernotFound()from that 3. usecache: 'no-store'only for the existence check if freshness matters more than caching Also worth logging inside the revalidation fetch wrapper to confirm it returnsnull` and does not throw on the 204/404 path.
Small correction to my inline snippet above: use next: { revalidate: 900, tags: ['ad:' + id] }, then call revalidateTag('ad:' + id) or revalidatePath(...) from the delete/update path.
Goldstripe sardinellaOP
@Spectacled bear thanks for the reply ☺️ Writing down my thoughts / questions on these
1. Wouldn't this require webhook (in our NextJS side logic), and on deletion - the API provider side should ping the webhook with the ad id - and then we would invalidate the tag?
2. This seems a bit hacky to me - returning status 200 from the API when data is not found - Solves the issue but doesn't sound right
3. Not sure how that works

Not sure if you were suggesting this on the option 2. - but do you think it could be good approach to create some kind of NextJS API route handler (proxy) that communicates with the external API - and based on the response it could then give a cacheable status code to NextJS side logic - based on the data. E.g. if API returns 204 - we could then return status 200 with body e.g. {deleted: true} from the API route handler to the NextJS logic
Spectacled bear
Yes — if you need deletion to be reflected immediately, the system that knows about the deletion has to notify Next somehow. A provider webhook into a Next Route Handler that calls revalidateTag('ad:' + id) / revalidatePath(...) is the cleanest version. Time-based revalidation alone is only eventual, and if the background refresh path does not produce a cacheable replacement, it can keep serving the last good 200.

The “200 tombstone” idea is not really pretending the upstream succeeded; it is a domain-level facade: upstream 200 -> { state: 'active', ad }, upstream 204/404 -> { state: 'deleted' }. Then your page can call notFound() from state === 'deleted', and Next has a cacheable value that can replace the old ad.

A Next route handler/proxy is reasonable if it is your app boundary, but I would still keep upstream HTTP semantics there, map them to a domain result for the app, and prefer webhook + tag invalidation when deletions must be timely. If no webhook exists, use a shorter revalidate window or no-store just for the existence check.