Skip to content

fix(cache): make on-demand tag invalidation immediate (new albums weren't showing in real time)#514

Open
Zheaoli wants to merge 1 commit into
mainfrom
be-album-revalidate
Open

fix(cache): make on-demand tag invalidation immediate (new albums weren't showing in real time)#514
Zheaoli wants to merge 1 commit into
mainfrom
be-album-revalidate

Conversation

@Zheaoli
Copy link
Copy Markdown
Collaborator

@Zheaoli Zheaoli commented Jun 3, 2026

Symptom

Creating a new album did not update the public home page in real time — the new album only appeared after up to an hour.

Root cause

The on-demand invalidation helpers in server/lib/cache.ts called revalidateTag(tag, 'max'). In Next 16 the two-argument form with a cacheLife profile is stale-while-revalidate, not immediate:

  1. 'max' resolves to the cacheLife { stale: 300, revalidate: 30d, expire: 365d } (config-shared.js).
  2. On flush, Next derives durations = { expire: 31536000 } and passes it to the cache handler (revalidation-utils.js). Its own comment: "If profile is not found and not 'max', durations will be undefined which will trigger immediate expiration."
  3. The file-system cache handler stores expired = now + expire * 1000 — i.e. one year in the future (file-system-cache.js). Only revalidateTag(tag) with no profile stores expired = now.
  4. areTagsExpired treats an entry as expired only once expiredAt <= now, so the tag is merely marked stale, never hard-expired.

The home page album nav (cachedAlbumsShow, tag albums) is a hot entry, so it kept being served and was refreshed only by its own unstable_cache TTL — 1h for albums/config. Hence "not real-time". This also matches the observation that a new image sometimes appeared sooner (a cold/evicted entry refetches fresh) while the new album stayed stale (hot nav entry, only marked stale).

This affects all three public cache groups (gallery / albums / config); the albums/config 1h TTL just made it most visible there. gallery already has the 60s TTL from #513, which masked it for images.

Fix

Route all invalidation through a single expireTag helper that passes { expire: 0 }:

  • { expire: 0 } → handler stores expired = nowimmediate hard expiration → next read refetches.
  • Type-correct (CacheLifeConfig = { expire?: number }), and no deprecation warning (unlike the single-arg form).
  • updateTag would also be immediate but throws outside a Server Action; our writes run inside the Hono route handler (app/api/[[...route]]/route.ts), so it is not usable here.

Scope / risk

  • Single file. Behaviour change is strictly more correct: tags now hard-expire on admin writes instead of being marked stale with a one-year window.
  • No change to TTLs, render mode, or the auth model. No new TypeScript errors in the touched file.

Note on a shared cacheHandler

This fix is required regardless of cache backend. A shared/persistent cacheHandler (for multi-instance propagation) would still receive the one-year duration under 'max' and not expire immediately, so dropping 'max' is a prerequisite. Whether a shared cacheHandler is additionally needed depends on whether the deployment runs multiple instances — tracked separately.

…e-revalidate

Creating a new album (and other admin writes) did not update the public
pages in real time: a new album only appeared on the home page after up
to an hour.

Root cause is the `'max'` argument passed to `revalidateTag`. In Next 16
`revalidateTag(tag, 'max')` is stale-while-revalidate, not immediate:
the `'max'` cacheLife resolves to `{ expire: 31536000 }` (one year), and
the cache handler stores `expired = now + expire * 1000` — a year out.
`areTagsExpired` only treats an entry as expired once `expiredAt <= now`,
so the tag is merely marked stale, never hard-expired. A hot entry such
as the home page album nav (`cachedAlbumsShow`) keeps being served and is
refreshed only by its own `unstable_cache` TTL, which for albums/config
is 1h. That is the "not real-time" behaviour.

Pass `{ expire: 0 }` instead, via a single `expireTag` helper. That sets
`expired = now` → immediate hard expiration → the next read refetches.
`updateTag` would also be immediate but throws outside a Server Action,
and our writes run inside the Hono route handler, so it is not usable;
the deprecated single-arg `revalidateTag(tag)` is immediate too but logs
a warning on every call. `{ expire: 0 }` is type-correct (CacheLifeConfig),
warning-free, and works from the route handler.

Note: this is required regardless of cache backend. A shared/persistent
cacheHandler (for multi-instance propagation) would still receive the
one-year duration under `'max'` and not expire immediately.
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
picimpact Ready Ready Preview, Comment Jun 3, 2026 2:47pm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant