This post is for the web team or marketing technologist at a hospital whose doctor pages show OPD days and timings, and whose rosters change every day. A consultant is on leave on Thursday, a clinic moves from the second floor to the annexe, a visiting specialist adds a Saturday slot. The hospital information system (HIS) knows within minutes. The website often learns days later, or never, because updating it means a developer, a deployment or an editor remembering.
It answers a specific engineering question: how do you keep statically generated doctor pages accurate when the schedule changes daily, without rebuilding the whole site for each change? The method is on-demand revalidation in Next.js, driven by roster events from the HIS, with cache tags that map a change to exactly the pages it affects. The examples describe our approach to revalidation on Next.js builds in general terms; they contain no client data, and the numbers used are hypothetical.
It assumes you already use, or are considering, a Next.js App Router front end. The content model behind doctors and placements is a separate subject; see our headless CMS and provider bio pages services for that side.
Roster is not availability
Separate two things that are often confused.
Roster is the planned pattern: Dr A consults at Location 2 on Monday, Wednesday and Friday from 10:00 to 13:00 in room 214. It changes a few times a month per doctor, occasionally daily across a large staff.
Availability is the live state of bookable slots on a given date. It changes every few minutes as patients book.
The first suits static generation with on-demand updates. The second does not. Do not try to bake availability into a cached page; fetch it on the client or at request time from the scheduling system, and keep the roster page as the stable frame around it. A page can say "Mon, Wed, Fri, 10:00 to 13:00" statically and load "next available: Friday" from an API. If the API is slow or down, the static roster still shows. This split is also kinder to your Core Web Vitals, since the static HTML paints quickly.
Our appointment scheduling build follows the same split.
How Next.js revalidation works, as of the current docs
The Next.js ISR guide describes the cycle. A page is prerendered and cached. After invalidation, the next request still receives the cached (stale) page while a fresh version generates in the background; once ready, later requests receive the new version. Time-based revalidation (export const revalidate = 3600) invalidates after an interval. On-demand revalidation invalidates when you call a function.
Two functions matter:
revalidatePathinvalidates specific paths or layouts.revalidateTaginvalidates all cached data carrying a tag, across every page that uses it.
The `revalidateTag` reference gives the current signature: a tag and a second argument. The recommended max profile marks data stale and serves stale content while revalidation runs in the background. The single-argument form is deprecated. { expire: 0 } means stale content is never served and the next request is a blocking revalidation, the right choice where the caller needs the data gone immediately and updateTag is not available, which is the case for webhooks and calls from other services. Tags are case-sensitive and limited to 256 characters.
An important point from the documentation: a revalidation is triggered by a request, not by the revalidateTag call. Pages refresh as they are visited. A rarely visited doctor page will stay stale in cache until someone requests it, then refresh. That is fine for most pages, because the first visitor after the change may still see the old version under max, which is a trade-off to consider for rosters.
Choosing stale-while-revalidate or immediate expiry
The profile you choose encodes a business rule about how wrong a page is allowed to be.
| Change | Risk if shown stale | Suggested handling |
|---|---|---|
| Added consultation day | Patient does not see a new option; low harm | max (stale while revalidating) |
| Time shifted by an hour | Patient arrives at the wrong time | { expire: 0 } |
| Doctor on leave or clinic cancelled | Patient travels for nothing | { expire: 0 } and a visible notice |
| Room or floor changed | Patient goes to wrong place | { expire: 0 } |
| Bio or photo edit | Cosmetic | max |
For schedule reductions and changes, prefer immediate expiry. The cost is that the next visitor waits for a fresh render rather than being served stale content. For a doctor page that renders in a fraction of a second, this is acceptable. If roster-critical pages are heavy, trim them rather than accept stale clinic times.
Tag design
Tags make the HIS event map to pages with no knowledge of URLs. Define at the data-fetching layer, not in the page:
roster:doctor:{doctorId}on any fetch that reads that doctor's scheduleroster:location:{locationId}on any fetch that lists doctors at a locationroster:department:{departmentId}on department listings that show "available this week"roster:allon the rare global components, such as a "doctors on duty today" widget
A fetch is tagged with next: { tags: [...] }, and functions using 'use cache' call cacheTag. The reference page shows both forms. One roster change event for Dr A at Location 2 triggers roster:doctor:A and roster:location:2. The doctor profile, the location page, and any department listing that includes her are all invalidated. No other page is touched.
Keep tags coarse enough to maintain. Tagging every field separately creates a map nobody can reason about. One tag per entity per concern is usually enough.
The pipeline from HIS to page
A typical flow:
- Event source. The HIS or the scheduling module emits a change, or you poll for changes.
- Normaliser. A small service converts the vendor format into your own event:
{ type: "roster.changed", doctorId, locationId, effectiveFrom, changeKind }. It also strips anything that is not roster data. The event must not contain patient information, and nothing patient-related should ever reach the website layer. - Queue or buffer. Events are queued, so a burst of changes (a department-wide rota update at 6 pm) does not flood the site.
- Revalidation endpoint. A Route Handler receives the event over HTTPS, checks a shared secret, validates the payload shape, maps
changeKindto a profile, and callsrevalidateTag. - Verification. Optionally, request the affected URLs and check the response.
Many hospital HIS products do not offer webhooks. If yours does not, run a poller that reads the roster export every few minutes, hashes each doctor's schedule, compares with the last hash, and emits an event only for those that changed. This is simple and reliable. The cost is latency equal to the polling interval, so choose the interval according to the kinds of change in the table above.
Document who owns the integration. When a vendor changes an export field, the poller breaks silently unless something is watching for it. Add an alert when the poller has produced no events for an unusually long time, since a quiet day and a broken feed look alike.




