Headless WordPress, ISR vs SSR: pick the rendering mode by content cadence

Headless WordPress, ISR vs SSR: pick the rendering mode by content cadence

Last verified: October 16, 2026
12 min read
Guide
500+ WP projects
Core Web Vitals

#Headless WordPress, ISR vs SSR: pick the rendering mode by content cadence

The “ISR or SSR” question only makes sense per-route. There is no whole-site answer. Astro and Next.js both let you choose mode at the page or layout level, and the senior-engineering move is to choose deliberately, route by route, against a content-cadence model.

This article anchors to the Headless WordPress service pillar and pairs with the Next.js vs Astro decision matrix, which covers the framework-level choice.

#TL;DR

  • ISR (or static + revalidation) wins when content cadence is predictable and traffic is high.
  • SSR wins when the page is personalised, session-driven, or contains live data.
  • Cache invalidation drives ISR correctness; webhooks beat time-based revalidation.
  • Cloudflare Workers can serve both through the framework adapter; a cached page pays almost no CPU, SSR pays the full render.
  • The default is the cheapest mode that satisfies correctness; promote to SSR only when needed.

#SSG, ISR and SSR explained for WordPress

Static / Static Site Generation (SSG). The page is built once, at build time, and served as flat HTML. Cheapest at request time, slowest to update.

Incremental Static Regeneration (ISR). The page is built once but can be regenerated on a trigger, usually a webhook on publish or a time-based revalidation interval. Cheap at request time, eventual consistency on update.

Server-Side Rendering (SSR). The page is rendered on every request. Always fresh, but the runtime cost scales with traffic. Personalisation, authentication, and live data fit naturally here.

In headless WordPress, all three read from the WordPress origin via REST or GraphQL. The difference is when they read.

#When to use ISR vs SSR in headless WordPress

Two inputs matter:

Content cadence. How often does this page change? Once a quarter, once a day, every minute, in real time?

Personalisation surface. Does this page differ per visitor? Logged-in state, location-aware pricing, A/B test variant.

The rule: pick the cheapest mode that satisfies correctness. Static is cheapest. SSR is most expensive. Promote toward SSR only when correctness fails on a cheaper mode.

Page typeDefault modeWhy
Marketing pages, blog postsStatic (rebuild on publish)Low cadence, no personalisation
Category and tag archivesISR with publish webhookCadence tied to content publish
Product pages, stable catalogueISR with stock webhookPredictable invalidation
Product pages, real-time stockSSR with edge cacheStock changes within seconds
Cart and checkoutSSRSession-driven by definition
Authenticated dashboardSSRPer-user state
Editorial homepageISR with publish webhookCadence tied to publish events

#ISR cache invalidation with WordPress webhooks

ISR sounds free until it serves a stale canonical URL after a slug change. The pattern that prevents that:

Webhook-driven invalidation. The WordPress origin sends a webhook on publish, slug change, or post deletion. The front-end framework consumes the webhook and triggers a regeneration of the affected pages. The cost is one webhook integration on the WordPress origin, paid once.

Time-based revalidation as a backstop only. Setting a 60-second revalidation interval covers webhook delivery failures but should not be the primary trigger. A 60-second interval does not rebuild every page 60 times an hour: Next.js regenerates a page only when a request arrives after the window. The real cost is that every popular page can regenerate up to once a minute in the background, and on platforms with per-request billing that background work counts as extra compute.

Cache tags, not URLs. Every cached page is tagged with the WordPress post ID, the term IDs it references, and any cross-cutting tags (homepage, sitemap). When a webhook arrives, the front purges by tag, not by URL. This is the difference between “regenerate the product page” (fragile) and “regenerate everything that references product 8421” (correct).

#ISR vs SSR cost on Cloudflare Workers

Astro reaches Workers through its official Cloudflare adapter. Next.js documents ISR for the Node.js runtime and lists adapter support as platform-specific, so check the adapter before planning ISR on Workers. The mode-by-mode cost picture:

  • Static at the edge. Cloudflare Pages serves flat HTML for almost no CPU per request. Cheapest mode.
  • ISR. First request after invalidation pays the full render cost; cached requests pay almost nothing. On Workers this depends on the adapter’s cache, not on the framework alone.
  • SSR. Every request pays the full render cost on Workers. Predictable per-request, expensive at scale.

Per the headless economics article, the cost difference matters at high traffic. At low traffic, the choice is correctness, not cost.

#ISR vs SSR examples for real WordPress routes

Marketing homepage. Static, rebuilt on every editorial publish via webhook. Cache for 24 hours at the edge with manual purge override. SSR fallback only if a country-specific banner is added.

WooCommerce product detail page. ISR keyed by product ID. Webhook from WooCommerce on stock change, price change, or content update. Cache window: 1 hour as backstop. SSR only if real-time stock display is a UX requirement.

Customer order history. SSR. Per-user, session-driven, no caching at the edge.

The same architecture; three different rendering modes; one decision rule.

#How Next.js revalidate works for WordPress pages

A route with export const revalidate = 3600 serves its cached HTML to every request inside the window. The current Next.js ISR guide describes what happens next: the first request after the window still gets the stale page, that request starts a regeneration in the background, and once it succeeds the following request gets the new version. A post nobody visits is never regenerated. The interval caps how often a page can rebuild; it does not schedule rebuilds.

Four details from the same guide decide whether this works on a WordPress front:

  • When a route makes several fetch calls with different revalidate values, the lowest one sets the ISR interval for the page. A sidebar that loads recent posts with a 60-second window drags the whole article down to 60 seconds.
  • One fetch with revalidate: 0 or cache: 'no-store' makes the route dynamic. A single uncached call to a WordPress endpoint, such as a view counter, silently moves the page from ISR to SSR.
  • If regeneration throws, for example because the WordPress origin returns an error, Next.js keeps serving the last good version and retries on the next request. An origin outage does not blank the front end.
  • The guide recommends a long interval, an hour rather than a second, plus on-demand revalidation when you need precision. That is the webhook-first rule from the section above.

The x-nextjs-cache response header reports the state of each request as HIT, STALE, MISS or REVALIDATED. Reading it right after a publish is the fastest way to confirm that a webhook reached the front end.

#How to revalidate Next.js pages from a WordPress webhook

The receiving end is a Route Handler, for example app/api/revalidate/route.ts, that checks a shared secret and calls one of two functions from next/cache.

revalidateTag(tag, 'max') marks every cached fetch carrying that tag as stale, across all pages that use it (revalidateTag reference). With the 'max' profile, visitors keep getting the stale copy while the fresh one loads. For a WordPress publish event that is usually the right trade: tag fetches with post-8421 and posts, and one webhook refreshes the post, the archive and the homepage. When the stale copy must never be served again, such as a retracted article, the reference tells webhook callers to pass { expire: 0 }, because updateTag only works inside Server Actions. The one-argument form revalidateTag(tag) is deprecated. Tags are case-sensitive and limited to 256 characters.

revalidatePath(path) works on routes instead of data (revalidatePath reference). A literal path such as /blog/my-post refreshes one page; a pattern such as /blog/[slug] needs the second argument, 'page' or 'layout'. Two traps are common in headless WordPress. With rewrites in next.config.js, pass the destination path, not the URL the visitor sees. And Proxy does not run for on-demand ISR requests, so a path rewritten there must be revalidated in its final form.

Both functions only mark entries; regeneration happens on the next visit. When the app runs on several instances with the default file-system cache, a webhook invalidates only the instance that received it, so on-demand revalidation needs a shared cache handler before it can be trusted.

#How Astro 7 route caching replaces ISR

Astro prerenders every page by default. A route opts into on-demand rendering with export const prerender = false, which requires a server adapter. output: 'server' flips the default, and the docs advise staying on 'static' until most pages really need a server (Astro on-demand rendering). An on-demand page can set its own Cache-Control header through Astro.response.headers.

Astro 7 adds route caching, which closes most of the gap with Next.js ISR (Astro route caching). An on-demand page calls Astro.cache.set({ maxAge: 3600, swr: 60, tags: ['post-8421'] }). The endpoint that receives the WordPress webhook calls context.cache.invalidate({ tags: ['post-8421'] }) or invalidate({ path: '/blog/my-post' }). Path invalidation matches exactly, without wildcards. routeRules in astro.config.mjs set defaults for groups of routes, for example a maxAge for /blog/[...slug], and a page can override them or opt out with Astro.cache.set(false) when the response is personalised.

The cache itself comes from a provider. The built-in memoryCache() suits a single Node server. The Netlify, Vercel and Cloudflare adapters ship CDN providers that are still experimental and must be enabled by hand. The Cloudflare provider, in @astrojs/cloudflare 14 and later, sets the Cloudflare-CDN-Cache-Control and Cache-Tag headers and turns on the Workers cache. In development the cache API does nothing, so test it with a build and a preview.

#How to mix static and dynamic parts on one WordPress page

Many routes are stable except for one live element: a product page with a stock counter, an article with the logged-in reader’s avatar. Rendering the whole page per request for that one element is the expensive answer. Astro server islands are the cheaper one (Astro server islands). Add server:defer to the component that needs fresh data. The rest of the page stays prerendered or cached, and the island is fetched from its own endpoint after the page loads. A child with slot="fallback" shows in its place until then, and each island loads independently, so a slow stock lookup does not hold up anything else.

Two limits apply. Server islands need an adapter, and their props must be serializable: plain objects, strings, numbers, arrays, dates and a few other types, but no functions. For a WooCommerce product page that means passing the product ID, not an API client.

This refines the table above. The route stays static or ISR, and only the island renders per request.

#How to fetch WordPress content for static and ISR builds

Every prerender step starts with a list of slugs, and the WordPress REST API returns it in pages. per_page accepts 1 to 100, and each response carries the X-WP-Total and X-WP-TotalPages headers, so a build script reads the first page and loops up to the page count from the header (REST API pagination). Next.js passes that list to generateStaticParams. Posts published after the build render on their first request unless dynamicParams is false, and generateStaticParams is not called again during revalidation (generateStaticParams reference).

The _fields parameter trims the response to what the step needs, for example ?_fields=slug,modified_gmt for the slug list, and WordPress skips computing the fields left out. Since WordPress 5.3 it also accepts nested meta keys, and since 5.4 _embed can be limited to named relations such as author,wp:term (REST API global parameters).

For change detection, the posts endpoint accepts modified_after with an ISO 8601 date and returns modified_gmt on every post (posts endpoint reference). A scheduled job that asks for posts modified since its last run is a sound backstop for missed webhooks, and cheaper than a short revalidate window on every route.

#How to preview WordPress drafts on an ISR front end

Editors expect the Preview button to show the draft, while a cached front end shows the published version. Next.js Draft Mode handles this per visitor (Draft Mode guide). A Route Handler checks a shared secret, calls draftMode().enable(), which sets the __prerender_bypass cookie, and redirects to the page. Requests with that cookie skip the fetch cache and the ISR response cache and are sent with Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate. Every other visitor still gets the cached page.

On the WordPress side, the posts endpoint returns published content unless told otherwise, because status defaults to publish. Draft reads are therefore authenticated requests. Application Passwords, built into WordPress since 5.6 and sent as Basic Auth over HTTPS, are the method the handbook documents (REST API authentication). Keep that credential on the server, in the Route Handler or a server-only fetch, and never in client code.

This long-tail article anchors to the Headless WordPress service pillar. For framework-level choice, see the Next.js vs Astro decision matrix. For migration-side risks, the SEO patterns checklist is the seven-point list. For commerce specifics, Headless WordPress for WooCommerce is the focused decision article. For a version-by-version framework comparison, including how each one loads WordPress data, read Astro 5 vs Next.js 15.

For the delivery side of this topic I keep the work under Core Web Vitals audit.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Want this implemented on your site?

If you are planning a Headless WordPress setup, frontend decoupling, or migration to Astro, I can design and build the architecture, API, and frontend.

Frequently asked questions

Practical answers to apply the topic in real execution.

Should I use ISR or SSR for product pages on headless WooCommerce#
ISR if stock and prices change less than once an hour and visitor count justifies caching. SSR if stock or pricing is real-time and personalisation is in the page. Mixed is fine: list pages on ISR, product detail on SSR with edge caching.
Does Astro support ISR#
Since Astro 7, in practice yes. Astro prerenders by default and renders routes on demand when an adapter is installed. Route caching in Astro 7 adds maxAge, stale-while-revalidate and invalidation by tag or path to those on-demand routes, which covers the same workloads as Next.js ISR. On older Astro versions the equivalent is a webhook-triggered rebuild plus a cache layer at the edge.
Where does Cloudflare Workers fit in this decision#
Workers can serve both modes through the framework adapter. Astro has an official Cloudflare adapter with an experimental CDN cache provider; Next.js documents ISR for the Node.js runtime, so on Workers it depends on what the adapter supports. The runtime cost difference is in CPU milliseconds per request: cached pages pay almost nothing, SSR pages pay the full render cost. For high-traffic sites the cumulative effect matters; for low-traffic sites it does not.
Can ISR break SEO#
It can. The risk is serving stale canonical URLs or stale meta tags after a slug change. Mitigation: webhook-trigger a regeneration on every WordPress publish, and set a short maximum stale window for pages whose metadata can change.
What is the simplest decision rule#
Start every route as ISR or static. Promote to SSR only when the page is personalised or session-driven. Demote back to static when the need goes away. The cheapest mode is the right default.

Need an FAQ tailored to your industry and market? I can build one aligned with your business goals.

Let’s discuss

Related Articles

Cloudflare Workers and WordPress: serving WooCommerce at the edge

Cloudflare Workers runs JavaScript and WebAssembly at hundreds of data centres in 100+ countries worldwide. Pairing Workers with a WordPress origin moves the read path off the WordPress server and turns WooCommerce into an edge-rendered store. Here is how the architecture works, where it breaks, and what to measure before adoption.