ISR — revalidation windows

Four cached scopes at four speeds, against upstreams that move at roughly matching rates: crypto prices every few seconds, station measurements every fifteen minutes, reference rates once a weekday, and repository counters continuously.

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: the stamp does not move — you were served the stale entry while regeneration kicked off in the background. Reload again: now 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 the upstream’s publication interval. “The cache entry is fresh” and “the data is new” are different claims, and the FX panel makes the same point more starkly — 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
73ms

ran at exec a3629sage …

  • bitcoin$84,342 · published 21:02:40Z
  • ethereum$2,688.02 · published 21:02:30Z

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 only 60 requests an hour, so shortening it is the quickest way to get a 403 here.

Station — 60s window

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

ran at exec 81mg1iage …

station London

temperature
17.7°C
humidity
71%
wind
8.3km/h
pressure
1020.3hPa
elevation
16m
observed
2026-09-24T21:00Z

The upstream publishes on a 15 minute interval, so the observed time changes far less often than this cache does. Two different freshness clocks, which is worth keeping straight.

FX rates — 5m window

use cache
cacheLife
blog
cacheTag
fx
upstream
396ms

ran at exec 4247jnage …

1 USD · published 2026-09-24

  • CHF0.82775
  • EUR0.87974
  • GBP0.75645
  • JPY158.85

ECB reference rates publish once per weekday. A 5 minute window over daily data is mostly wasted work — which is the point: match the window to the upstream, not to a habit.

Repo counters — 1h window

use cache
cacheLife
hours
cacheTag
repo
upstream
191ms

ran at exec yjwurrage …

stars
142,426
forks
32,786
open issues
3,481
pushed
2026-09-24T21:04:48Z

A rate-limited upstream is the textbook case for a long window. A 403 here means the hourly quota is gone; 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 two buttons differ only in whether a stale read is permitted first — which is precisely the distinction the two-reload rule above is about.

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 very next reload blocks on a fresh fetch and the stamp moves immediately.
  • revalidateTag('stations:london', { expire: 0 }) — Same thing for the station panel — note the other panels are untouched.