ISR revalidation windows

Four cached scopes on four windows, over upstreams that change at different rates: crypto prices every few seconds, station measurements every fifteen minutes, reference rates once a weekday, and repository counters whenever the repo changes.

Watch each panel's age, which ticks live in your browser. When it passes the profile's revalidate value, the entry is eligible for regeneration.

What to check

The two-reload rule. Let the market panel's age pass 15s. Reload once and the stamp does not move, because you got the stale entry while regeneration ran in the background. Reload again and it moves.

Cached against live. The two market panels read the same endpoint in the same request. Their prices drift apart within the window and re-converge when the cached one regenerates.

Two clocks. The station panel has a 60s cache window but its observed time only advances every 15 minutes, because that is how often the upstream publishes. A fresh cache entry does not mean new data. The FX panel shows the same thing with a 5 minute window over data that changes once a weekday.

From the shell. for i in $(seq 1 6); do curl -s localhost:3000/isr | grep -m1 -o 'data-exec="[^"]*"'; sleep 5; done. The id repeats, then changes, on that two-request cadence.

Fast window, 15 seconds

cacheLife('isr-15'): stale 30s, revalidate 15s, expire 10m. The left panel is cached; the right one is the same fetch with no cache at all, rendered in the same request.

Market — cached 15s

use cache
cacheLife
isr-15
cacheTag
market
upstream
85ms

ran at exec m2fgrjage …

  • bitcoin$84,571 · published 21:29:50Z
  • ethereum$2,693.33 · published 21:29:50Z

Tagged 'market', so the buttons below can expire it without waiting for the window.

Market — live

suspense fallback

streaming at request time. This is what ships in the static shell.

Medium and slow windows

Station measurements at 60s, reference rates at 5m, and repository counters at 1h. The GitHub window is long on purpose. Unauthenticated GitHub allows 60 requests an hour, so a shorter window soon returns a 403.

Station — 60s window

use cache
cacheLife
isr-60
cacheTag
stations, stations:london
upstream
490ms

ran at exec 6hsmo1age …

station London

temperature
16.3°C
humidity
63%
wind
10.1km/h
pressure
1026.4hPa
elevation
16m
observed
2026-10-01T21:30Z

The upstream publishes every 15 minutes, so the observed time changes far less often than this cache refills.

FX rates — 5m window

use cache
cacheLife
blog
cacheTag
fx
upstream
331ms

ran at exec wc1ukbage …

1 USD · published 2026-10-01

  • CHF0.83528
  • EUR0.88511
  • GBP0.75565
  • JPY157.98

The ECB publishes reference rates once per weekday, so a 5 minute window over them is mostly wasted work. Match the window to how often the upstream changes.

Repo counters — 1h window

use cache
cacheLife
hours
cacheTag
repo
upstream
236ms

ran at exec c78u8oage …

stars
142,988
forks
33,559
open issues
3,545
pushed
2026-10-01T21:23:37Z

A rate-limited upstream needs a long window. A 403 here means the hourly quota is used up. It resets on its own and nothing else depends on it.

Skip the wait

Each of these panels is tagged, so you do not have to sit through a window to see a regeneration. The first two buttons differ only in whether one stale read comes first, which is the two-reload rule above.

Invalidate an ISR window early

  • revalidateTag('market', 'max'): Stale-while-revalidate. The next reload still shows the OLD stamp; the one after shows the new one.
  • revalidateTag('market', { expire: 0 }): No stale window. The next reload blocks on a fresh fetch and the stamp moves.
  • revalidateTag('stations:london', { expire: 0 }): The same for the station panel. On the Pantheon handler an immediate invalidation re-runs every cached scope on the route, so the other cached panels move too.