You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Document how to emulate Vercel-style Incremental Static Regeneration
with defineCachedHandler (swr + omitted staleMaxAge), on-demand
revalidation via .expire()/.invalidate(), per-route windows via
getMaxAge, and the two divergences from managed ISR.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`shouldCache` receives the serialized response entry, may be async, and is **ANDed** with the built-in checks — it can only narrow what gets cached, never force-cache a response the built-ins reject. It gates both storing a fresh response and serving a stored one, and a throwing hook fails closed (treated as non-cacheable) and is reported via `onError`.
192
192
193
+
#### Incremental Static Regeneration (ISR)
194
+
195
+
you can reproduce a similar ISR behavior with `defineCachedHandler`: serve a cached page instantly, regenerate it in the background after it goes stale, and keep serving the last-good version until the refresh lands:
196
+
197
+
```ts
198
+
const page =defineCachedHandler(
199
+
async (event) => {
200
+
const html =awaitrenderPage(event.url??newURL(event.req.url));
swr: true, // serve stale instantly, refresh in the background
205
+
maxAge: 60, // "revalidate" window: fresh for 60s, then refresh on next request
206
+
// no staleMaxAge → stale is served indefinitely until the refresh succeeds
207
+
},
208
+
);
209
+
```
210
+
211
+
The two options that make it ISR-like:
212
+
213
+
-**`swr: true`** turns on stale-while-revalidate: once an entry is older than `maxAge`, the next request gets the stale page immediately while a fresh render runs in the background.
214
+
-**Omit `staleMaxAge`.** This is the important part. Leaving it unset means there's no point at which the entry becomes "too old to serve" — the last successful render is served forever until a refresh replaces it, exactly like ISR. (If instead you _set_`staleMaxAge`, you get a hard cutoff: after `maxAge + staleMaxAge` the entry is dropped and the next request blocks on a fresh render.)
215
+
216
+
With this config the handler also emits `Cache-Control: s-maxage=60, stale-while-revalidate`, so any shared/CDN cache in front of it revalidates on the same schedule.
217
+
218
+
**On-demand revalidation** (the equivalent of `revalidatePath` / `revalidateTag`) uses the methods on the returned handler:
219
+
220
+
```ts
221
+
awaitpage.expire(event); // ISR-style: serve the stale page once more, refresh in the background
222
+
awaitpage.invalidate(event); // hard purge: next request blocks on a fresh render
223
+
```
224
+
225
+
Prefer `.expire()` for the ISR feel — there's no blocking gap for visitors. Reach for `.invalidate()` only when the next reader must get a guaranteed-fresh render.
226
+
227
+
**Per-route revalidate windows.** If different pages need different refresh intervals (like Next's per-fetch `revalidate`), use `getMaxAge` to derive the window from the response — for example an `x-revalidate` header your handler sets. `entry.value` is the standard `Response`:
> Two things differ from CDN managed ISR. **(1) Background refresh is coalesced per instance**, not globally — across multiple servers/serverless instances the origin can see one refresh per instance. Add a distributed lock in your [custom storage](#custom-storage) if regeneration is expensive. **(2) Entries never auto-expire** with `staleMaxAge` omitted, so storage grows until you `.invalidate()` — or set a large `staleMaxAge` to trade exact ISR semantics for eventual cleanup.
247
+
193
248
### Cache Invalidation
194
249
195
250
Cached functions have an `.invalidate()` method that removes cached entries across all base prefixes:
0 commit comments