The rendering strategy decision record: why the rendering model you chose determines your hydration failure surface and your edge caching consistency contract
Rendering strategy decisions are made in three founding sessions that never document the operational consequences — the "use React for the storefront" session that picks client-side rendering without specifying the crawlability requirement for the pages that drive organic search traffic, so that product listing pages ship React bundle shells to Googlebot for 18 months while organic traffic accrues zero indexed content from the pages that were supposed to be the company's primary customer acquisition surface; the "add ISR to the pricing page" session that sets revalidate: 3600 without specifying the acceptable staleness window for content categories where a stale value creates a customer obligation, so that a sales rep shows a customer the cached $499/month price in a demo, the customer signs expecting that price, and the contract dispute surfaces when the live pricing API charges $599/month and legal has to determine whether the displayed price constitutes a binding commitment; and the "move the dashboard to edge functions" session that deploys server-side rendering to geographically distributed edge nodes without specifying the database read model, so that users in Europe see their preferences rendered from a Frankfurt read replica with 15-second replication lag for up to 15 seconds after every save — the page appears to ignore their changes, their support tickets describe "settings not saving," and the investigation requires correlating the timestamps of writes to the US primary against the timestamps of SSR requests served from the Frankfurt node. What none of these sessions produce is the crawlability specification for the rendering model, the acceptable staleness window per content category for incremental static regeneration, or the database read consistency contract for edge-rendered user-specific pages.
A 30-person e-commerce SaaS company built a hosted storefront product that let merchants create product catalogs and sell through a managed online store. The founding engineering team had strong React experience and picked a client-side rendering architecture: a Next.js application in client-only mode where all pages were rendered by the browser after fetching data from a REST API. The choice was practical — the team knew React, the data fetching patterns were straightforward, and the developer experience of a single-page application was familiar. The storefront loaded correctly in every browser the team tested. Product titles, images, prices, and descriptions appeared as expected. The experience felt fast on the team's machines and on the demo environments they showed to early customers.
The company signed its first paying customers in the second month after launch. Merchants created product catalogs. Products were listed. Customers could browse and purchase. Growth came initially from the merchants' own social promotion and email lists — direct traffic that landed on the storefront via shared links, not via search. The engineering team focused on merchant tooling: inventory management, order processing, discount codes, shipping integrations. The storefront product's search engine presence was not a metric anyone measured in the first year, because the customers who were signing up were coming through direct referrals and the founders' outreach, not through organic search.
Eighteen months after launch, the company hired its first growth engineer. The growth engineer's first week included an audit of the company's search presence. The URL Inspection tool in Google Search Console showed the rendered HTML that Googlebot had indexed for the storefront pages. For a merchant with 4,000 products across 80 category pages, the Indexed HTML for each category page contained the page's title tag, the meta description set in the <head>, the navigation markup, and the footer — and a single <div id="__next"></div> in the body, the root element that the React application would mount into when the JavaScript bundle executed. No product names. No product descriptions. No prices. No structured data from the JSON-LD schema that the engineering team had added to each product page. The 4,000 product pages were individually indexed as nearly empty HTML shells, because Googlebot had crawled them in its primary indexing pass (which does not execute JavaScript for budget-constrained crawls) and received only the empty root div.
The growth engineer queried Google for the merchant's domain: site:merchant.whychose-storefront-example.com. The search returned 87 indexed pages, all showing only the title and meta description in the snippet, with no product content visible in any snippet body. The merchant had 4,000 products that had been live for 14 months. Zero of them were appearing in Google's product search results. The organic search acquisition surface — the surface that would have driven product discovery for the merchant's end customers — had been zero from the day the first product was created. Every merchant on the platform was in the same position. The engineering team's product had been silently absent from search for 18 months.
The fix required migrating the storefront to server-side rendering: each product page and category page would be rendered on the server for each request, returning complete HTML to Googlebot and to end-user browsers. The migration was not a configuration change — the data fetching patterns, component architecture, and state management model all assumed client-side execution and required restructuring for getServerSideProps. The migration took 4 months of engineering time. During those 4 months, Googlebot continued to index the shell pages. Full indexing of the migrated pages took a further 3–4 months after the migration completed, as Googlebot re-crawled and re-indexed each page. The founding session that picked client-side rendering had not documented the crawlability requirement for the product listing and detail pages — the requirement that these pages must produce their full content in the initial HTTP response, not in JavaScript-rendered output, because the primary acquisition channel for the merchants was organic search, and organic search acquisition requires crawler-accessible content.
A 25-person B2B project management SaaS company migrated their marketing pages and pricing page from a static HTML site to Next.js. The engineering team chose Incremental Static Regeneration for the pricing page: the page was pre-rendered at build time and regenerated in the background when a request arrived after the revalidation window expired. The revalidation window was set to revalidate: 3600 — one hour. The choice was documented in a PR description as "ISR for pricing page, revalidates every hour." The rationale was performance: the pricing page was one of the highest-traffic pages on the marketing site, and the team wanted to serve it from the CDN edge without incurring a database query on every request. The ISR model provided that: the CDN served cached HTML, and the background revalidation kept the content reasonably fresh.
The pricing page had three plans: Starter ($49/month), Professional ($99/month), and Enterprise ($499/month). The pricing was displayed on the page and used by the sales team in customer demos. Nine months after the Next.js migration, the company decided to change its pricing. The Enterprise plan was repriced to $599/month — a $100 increase justified by new SSO and compliance features added in the previous quarter. The pricing manager updated the content in the CMS at 9:04 AM on a Monday. The CMS webhook triggered a Next.js ISR on-demand revalidation for the pricing page — a feature the engineering team had intended to configure but had not yet implemented. The webhook call failed silently (the endpoint was not registered) and no revalidation occurred. The pricing page continued to serve the cached $499/month price from the CDN.
At 9:47 AM, a sales rep was on a video call with a potential Enterprise customer, sharing their screen and walking through the pricing page. The page showed $499/month. The customer was comparing against a competitor priced at $550/month and found the $499 price compelling. The customer asked the sales rep to confirm the price. The sales rep confirmed: "$499 per month for the Enterprise plan, yes." The customer said they would proceed. A contract was sent that day. The contract's pricing was generated from the live pricing API, which the engineering team had updated when they deployed the $599 pricing change at 9:02 AM — before the CMS content change, as part of the same release. The contract showed $599/month.
The customer received the contract at 2:30 PM and replied that the price in the contract did not match what they had been shown at 9:47 AM. They attached a screenshot of the pricing page from the demo, which showed $499. The sales rep escalated to their manager, who escalated to the VP of Sales, who escalated to the CFO. Legal was consulted on whether the $499 price displayed during the sales demo constituted a binding commitment. The investigation determined that the ISR cache had served the stale $499 page until 11:12 AM — when the hourly revalidation window expired and the background job regenerated the page with the $599 price. The sales demo occurred at 9:47 AM, within the hour window when the cache held the pre-change price. The CMS webhook had been configured to call the revalidation endpoint, but the endpoint had not been registered in the Next.js application — the infrastructure existed in the CMS, but the application-side handler was missing, and the failure was silent because the webhook returned an HTTP 200 from the CDN (the path existed but was not handled by the application layer). The founding session that picked ISR for the pricing page had documented the revalidation interval as a performance choice. It had not specified the acceptable staleness window for pricing data — the maximum duration for which an outdated price could be served before a customer-visible error occurred — or whether pricing data required on-demand invalidation with verified delivery confirmation rather than time-based revalidation.
A 20-person global B2B SaaS company built its user workspace dashboard using server-side rendering hosted in a single AWS region (us-east-1). As the company's customer base grew to include enterprise customers in Europe and APAC, dashboard load times for those users increased: time-to-first-byte from the single origin averaged 820ms for users in Frankfurt and 1,100ms for users in Singapore. The engineering team evaluated edge rendering as a solution: the server-side render would execute in Vercel Edge Functions, which ran in the datacenter closest to each user. Frankfurt users would receive their SSR response from a Frankfurt node, reducing TTFB to 45–70ms.
The migration to edge SSR was planned and executed over six weeks. The dashboard's server-side render function fetched the user's workspace data, recent activity, and preferences using the user's session token. In the origin architecture, the fetch target was the PostgreSQL primary database in us-east-1. In the edge architecture, the render function ran in Frankfurt — which had no direct connection to the us-east-1 primary. Connecting the Frankfurt edge function to the us-east-1 primary would have added the Frankfurt-to-Virginia round-trip latency to every database query, negating the TTFB improvement. The engineering team configured the Frankfurt edge function to fetch from a PostgreSQL read replica in Frankfurt (eu-central-1). The replica was configured with standard PostgreSQL streaming replication with an observed average lag of 4–8 seconds and a P99 lag of 15 seconds.
The migration shipped. European TTFB improved from 820ms to 62ms — a 13× improvement that showed up immediately in the engineering team's performance monitoring. The migration was considered a success. The engineering retrospective noted TTFB improvement as the sole metric of success.
Two weeks after the migration, the customer success team received the first support ticket from a European customer: "When I change my notification preferences, the changes aren't saving. I've tried three times." The engineer who investigated confirmed that preference writes were succeeding — the write request returned HTTP 200 and the database primary in us-east-1 had the correct updated value. The issue was that the next page load — a server-side-rendered response from the Frankfurt edge node — was fetching from the Frankfurt replica, which had not yet received the preference write from the primary. The rendered page showed the pre-update preferences. Reloading the page 20 seconds after saving showed the updated preferences (the replica had caught up). Reloading immediately after saving showed the old preferences. From the user's perspective, the save button did nothing.
The engineering team identified the root cause within 24 hours. The fix required a read-after-write consistency mechanism: when a user submitted a preference change, the server set a cookie recording the timestamp and a write token. The Frankfurt edge function, on receiving the next request, checked the cookie: if a recent write token was present, it fetched user data from the primary (paying the cross-region latency) rather than the Frankfurt replica. If no write token was present (the common case for users who had not recently written data), it fetched from the Frankfurt replica. The write token expired after 30 seconds, covering the maximum observed replication lag with a 2× safety margin. The migration to the consistency mechanism took 2 weeks. During those 2 weeks, support received 14 additional tickets from European customers about preferences "not saving." The founding edge rendering session had not documented the database read model — which source each category of user-specific data would be fetched from on edge nodes, what the replication lag bound was for the replica, and for which page categories and data types primary reads were required to guarantee read-after-write consistency.
Structural properties set by the rendering strategy decision
Three structural properties are determined when a team decides how to render a route. None appear explicitly in the sessions that choose a rendering framework or configure a revalidation interval — they are the operational consequences of choices made under the pressure of shipping pages that work in the environments where they are tested, where "work" means the content appears correctly in a browser with JavaScript enabled, the developer's machine is co-located with the database, and the test user does not scroll through Googlebot's indexed output to verify that the page content reached the index.
Property 1: The rendering model and the crawlability surface. A rendering model determines what the initial HTTP response contains. Client-side rendering sends a minimal HTML document: a <head> with the page's title and meta tags, a <body> with a single root element, and one or more <script> tags that reference JavaScript bundles. The page content — product names, descriptions, prices, article text, navigation links beyond the hard-coded shell — exists only in the JavaScript bundles. A client that executes the bundles receives the page content. A client that does not execute the bundles receives the shell.
Search engine crawlers index the initial HTTP response. Googlebot operates two indexing paths: a primary crawl that processes the raw HTML without JavaScript execution, and a secondary rendering queue that executes JavaScript for pages the primary crawl identifies as requiring rendering. The secondary queue has a budget constraint — not every page is rendered, and the queue prioritizes pages with high crawl priority (high PageRank, frequent update signals) over new or low-priority pages. Pages in the secondary queue may wait days or weeks before their JavaScript is executed. Pages that are never enqueued for JavaScript rendering are indexed with only the shell content — which, for a React SPA, contains no page-specific text. A product page indexed with only its title tag and meta description does not appear in Google's product search results, does not receive a rich snippet from its JSON-LD structured data (because the JSON-LD was written by JavaScript that did not execute), and does not rank for its product name, category, or description keywords.
Server-side rendering and static generation both send fully-rendered HTML in the initial HTTP response. The crawler receives the page content in the first HTTP response — no JavaScript execution required. The JSON-LD structured data is present in the <head>. The product names, descriptions, and prices are in the page body. The page can be indexed and ranked without the crawler executing any JavaScript. The crawlability property of a route is binary: either the initial HTTP response contains the page content that must be indexed, or it does not. The rendering strategy determines which side of that binary the route falls on. The CDN decision record documents the interaction between CDN caching and crawler indexing: static pages served from a CDN edge receive full content immediately, but CDN cache configuration must allow crawlers through without requiring JavaScript execution or cookies that block the crawler from seeing the cached response.
Property 2: The hydration model and the client-server consistency invariant. Hydration is the process by which React attaches client-side component state and event handlers to the server-rendered or statically-generated HTML. React's hydration expects that the component tree's render() output is identical on the server and the client for the same props. If the server-rendered HTML contains a node that the client's render() would not produce for the same props, React detects the mismatch, logs a hydration warning, and re-renders the mismatched subtree — discarding the server HTML for that subtree and replacing it with a fresh client render. The re-render causes a layout flash: the user sees the server-rendered content momentarily, then sees it replaced by the client-rendered content. For large subtrees, the flash is visible as a content shift that affects Core Web Vitals (CLS — Cumulative Layout Shift).
Hydration mismatches occur when a component reads from a source that exists only on the client. Common sources: window.innerWidth (the viewport width, which is undefined on the server), localStorage.getItem() (localStorage is a browser API, undefined on the server), Date.now() (the server and client execute Date.now() at different times — the server renders at request time, the client hydrates milliseconds to seconds later), Math.random() (the server and client produce different random values), and navigator.language (the user's browser language, unknown during server rendering). The invariant requirement is that every component in the server-rendered tree produces the same output for the same props on both server and client at the moment of hydration. Components that read client-only sources must either defer that read until after hydration completes (using useEffect for post-hydration side effects that do not affect the initial render) or suppress the hydration warning for the specific element using suppressHydrationWarning, which accepts the mismatch and preserves the server-rendered HTML without comparison.
suppressHydrationWarning is an escape hatch, not a solution. It tells React that the developer has accepted the mismatch and wants to preserve the server HTML for that element. The element's content is not updated by hydration — only by subsequent re-renders triggered by state changes or effects. If the server-rendered value is stale (a price that changed between the server render and hydration), suppressHydrationWarning on that element means the user sees the stale server-rendered value until a state change triggers a re-render. The state management decision record documents the interaction between server-state and client-state: components that display server-fetched data and use suppressHydrationWarning to avoid hydration mismatches must also implement a client-side revalidation strategy (SWR, React Query, or a manual fetch in useEffect) to ensure that stale server-rendered values are replaced with fresh client-fetched values after hydration.
Property 3: The edge rendering model and the data consistency contract. Edge SSR executes the render function in geographically distributed nodes. The data fetched during the render reflects the state of the data source at the time of the request, in the region where the edge node is running. If the data source is a regional read replica, the rendered HTML reflects the replica's state — which may lag the primary by the replication delay. The data consistency contract for an edge-rendered route specifies: which data sources the render function reads from (primary or replica for each data category), what the maximum acceptable lag is for each data category, and how read-after-write consistency is achieved for user-specific data written by the same user in the same session.
The consistency contract interacts with the routing model. An edge SSR route that always reads from the nearest replica provides the lowest TTFB but the weakest consistency guarantee: any data written to the primary by the same user in the same session may not be reflected in the next render for up to the replica's replication lag. An edge SSR route that always reads from the primary provides the strongest consistency guarantee but pays the cross-region round-trip on every data fetch — the TTFB improvement from moving the render to the edge is partially or fully offset by the primary read latency if the primary is in a distant region. A read-after-write consistency mechanism — using a session cookie or request header to indicate a recent write, routing requests with recent writes to the primary and requests without recent writes to the nearest replica — achieves both low TTFB for the common case (no recent write) and strong consistency for the post-write case. The database read replica decision record documents the replication lag model and the read-after-write consistency patterns that apply to any architecture where the application reads from a replica; edge SSR is a specific deployment topology where the application's read path is geographically distributed, making the replication lag problem more visible because users in the replica's region see the lag effect on every page load, not just on the occasional failover read.
What the founding session records and what it omits
The founding rendering strategy session typically records the chosen framework (Next.js, Remix, Astro, Nuxt), the rendering mode configured for the application (SSR, SSG, ISR, or the default), and the rationale — usually one of: performance, developer experience, the team's prior experience, or the requirements of a specific page type that drove the framework choice. The session may record which pages use which rendering mode if the application uses a mixed model (SSG for marketing pages, SSR for authenticated app pages). What it does not record is the crawlability requirement for each page category — whether the page's content must appear in the initial HTTP response for the page to fulfill its acquisition purpose. It does not record the hydration invariant — the set of components that must produce identical output on server and client, and the policy for components that read client-only sources (defer to useEffect, or use suppressHydrationWarning, and if the latter, what revalidation strategy ensures the suppressed element's stale server value is replaced). It does not record the acceptable staleness window per content category for ISR — the maximum duration for which a cached page may serve outdated content before the outdated content creates a user-visible error or a customer obligation. It does not record the database read model for edge-rendered routes — which data categories require primary reads, what the replica lag bound is, and how read-after-write consistency is achieved for user-specific writes.
The omissions produce different failure modes at different timescales. The crawlability omission produces a slow, invisible failure: organic search traffic that should exist simply never materializes. There is no error, no alert, no incident. The product works correctly for every user who arrives through direct links or paid channels. The SEO failure surface only becomes visible when someone measures organic search traffic against the expected baseline — a measurement that does not happen until the company has enough traffic to make the comparison meaningful, often a year or more after launch. By that point, the compounding benefit of early indexing (domain authority, backlink accumulation, historical ranking signals) has been foregone for the entire period.
The ISR staleness omission produces a sudden, contractual failure: a pricing discrepancy that requires legal review. The failure mode is triggered by a race between a content change and the revalidation window — a race that is inherent in time-based revalidation and inevitable over the lifetime of a product that changes its prices. The probability of a staleness-related incident increases with each pricing change: every price change is a race between the change taking effect in the live API and the ISR cache serving the old price to a user who will rely on the displayed price. The failure does not occur on every pricing change — only when a user takes a reliance action within the staleness window. But the probability is not zero, and for a product with active sales cycles, it is not negligible.
The edge rendering consistency omission produces a user experience failure that is reported as a product bug. Users who save their preferences and immediately reload the page see their changes "not saving" — a symptom that is indistinguishable from a write failure to the user and to first-line support. The investigation requires correlating edge render request timestamps with database write timestamps and replica lag measurements — an investigation path that requires knowing that the architecture includes edge rendering with replica reads, which is a piece of information that must be in the rendering strategy decision record to be available to the on-call engineer when the support ticket arrives at 11 PM.
The rendering strategy decision record does not need to enumerate every component's hydration behavior. It needs to answer four questions per route category: which rendering model is used (CSR, SSR, SSG, ISR, edge SSR), what crawlability guarantee the model provides and which routes require full content in the initial HTTP response, what the acceptable staleness window is for ISR routes per content category, and what the database read model is for edge-rendered routes including the primary/replica policy and the read-after-write consistency mechanism. Four answers written down in the founding session avoid the 18-month SEO absence, the pricing contract dispute, and the European users' preference save failures that each trace back to a rendering property that was not specified in the session that chose the model.
The WhyChose decision extractor finds the founding rendering sessions in your ChatGPT and Claude export — the "how should we structure the Next.js app?" conversation, the "should we use ISR for the pricing page?" thread, the "let's move to edge functions" session. It extracts the decision and the trade-off that was actually considered, not the surrounding framework comparison discussion that buries the rendering model choice in forty messages about bundle size, hydration APIs, and deployment configuration.
The five ADR sections for a rendering strategy decision
Section 1: Rendering model selection per route category. Enumerate the route categories in the application and specify the rendering model for each. Route categories are distinguished by their content update frequency, their crawlability requirement, their data personalization requirement, and their consistency requirement.
Client-side rendering (CSR) is appropriate for routes that are: (a) behind authentication (not crawled by search engines, so crawlability is not a requirement), (b) highly dynamic (the content changes on every user action or at high frequency, making pre-rendering impractical), and (c) served to users who are already in the application and have the JavaScript bundle cached. CSR is not appropriate for routes that drive organic search acquisition or that must display their content to link-preview scrapers (social media unfurling, Slack, iMessage).
Static site generation (SSG) is appropriate for routes where: (a) the content does not change between deployments (or can be treated as changed only at deployment time), (b) there is a bounded number of pages (SSG generates one HTML file per route at build time — 10,000 product pages is feasible; 10 million is not), and (c) the content does not vary by user (the same page is served to every visitor). Blog posts, documentation pages, and static marketing pages fit this profile.
Incremental static regeneration (ISR) is appropriate for routes where: (a) the content changes at a rate that makes full rebuild on every change impractical, (b) the content does not vary by user (the same cached page is served to every visitor — ISR cannot be used for personalized content without bypassing the ISR cache for authenticated requests), and (c) the acceptable staleness window (see Section 3) is longer than the minimum revalidation interval supported by the deployment platform. Marketing pages, product feature descriptions, and blog indexes fit this profile. Pricing pages, availability pages, and inventory pages require on-demand invalidation rather than time-based revalidation (see Section 3).
Server-side rendering (SSR) is appropriate for routes where: (a) the content varies by user (personalized dashboards, account pages, user-specific feeds), (b) the content must be fresh on every request (the page displays data that changes faster than any acceptable revalidation interval), or (c) the route must be crawlable with user-specific content (authenticated pages that are pre-rendered with default or public content for the crawler). SSR pays a server execution cost on every request — the route is not cached at the CDN edge unless the response includes explicit cache headers that make the CDN cache viable for some fraction of requests. The caching strategy decision record documents when SSR responses can be CDN-cached despite per-request execution: responses that vary only by locale or device type (not by user identity) can be cached with a Vary: Accept-Language, User-Agent header and served from the CDN for subsequent users with the same locale/device combination.
Edge SSR is SSR executed in geographically distributed nodes. It is appropriate for routes where SSR is the required rendering model (personalized, fresh, crawlable) and where the TTFB for users in distant regions is above the acceptable threshold. Edge SSR requires specifying the data consistency model separately (see Section 4). Document which routes use edge SSR, which edge regions are active, and the expected TTFB improvement by region. The frontend framework decision record documents the framework-level constraints on rendering model selection — not all frameworks support all rendering models for all route types, and the framework selection and the rendering model selection are coupled decisions that must be made together.
Section 2: Hydration specification — server-client identity invariant and component policy. Specify the hydration invariant: for every component in the server-rendered tree, the component's render() output must be identical on the server and the client for the same props at the moment of hydration. Document the sources that violate the invariant if read during rendering (not in an effect): browser APIs (window, document, navigator, localStorage, sessionStorage), time sources (Date.now(), new Date() in formats affected by timezone), random sources (Math.random(), UUID generation), and environment-specific values (viewport dimensions, media query matches).
Specify the policy for components that must read from these sources. Two valid patterns: (1) Deferred read: the component renders a default value (null, a loading state, or an empty placeholder) on both server and client during the initial render, then reads the client-only source in a useEffect that fires after hydration. The page displays the placeholder briefly, then updates to the client-specific value after hydration. This is the correct pattern for viewport-width-dependent layout, localStorage-based user preferences, and any value that is specific to the client environment. (2) Suppressed mismatch: the component uses suppressHydrationWarning on the specific element to tell React to preserve the server-rendered HTML for that element without comparison. This is appropriate for elements where the server-rendered value is acceptable to display briefly (a server-side timestamp that will be replaced by a client-side value within 100ms of hydration) and where the visual flash from preserving the server value is not user-visible. It is not appropriate for pricing values, user-specific data, or any value where displaying the server-rendered value after hydration constitutes incorrect information. Document the suppressHydrationWarning policy: specify which elements are permitted to use it and which are not, and specify the required post-hydration revalidation strategy for any element that uses it.
Specify the hydration error monitoring policy: hydration mismatches in development mode are warnings in the browser console; in production mode, Next.js and similar frameworks may suppress the console warning but the re-render still occurs, causing layout shift. The observability strategy decision record documents production monitoring for hydration errors; Next.js 13+ exposes hydration errors as React's onRecoverableError callback, which can be logged to an error tracking system to measure the rate and identity of hydration mismatches in production. Specify the alerting threshold: a non-zero hydration mismatch rate on a static page (where server and client environments should be identical) indicates a regression introduced by a component that reads a client-only source during rendering.
Section 3: ISR revalidation model — staleness window per content category and on-demand invalidation triggers. Specify the acceptable staleness window for each content category served via ISR. The staleness window is the maximum duration for which a cached page may serve outdated content before the outdated content creates a user-visible error or a customer obligation. Content categories and their typical staleness tolerance:
High staleness tolerance (days): static content that changes only with intentional editorial updates — blog posts, documentation pages, author bios, changelog entries. A blog post that has been edited since the user last visited will show the old version until the ISR cache expires; the user sees slightly outdated content, which is not an error and creates no obligation. Revalidation interval: 86,400 seconds (24 hours) or longer. On-demand invalidation: triggered by CMS publish webhook, with best-effort delivery (a webhook delivery failure results in the old content being served until the next time-based revalidation).
Medium staleness tolerance (minutes): content that changes on a business cadence but whose outdated version does not create a financial or legal obligation — product feature descriptions, team pages, job listings. A job listing that has been filled since the user last visited will show as open; the user may apply to a closed position, which is a poor user experience but not a legal commitment. Revalidation interval: 300–3,600 seconds (5 minutes to 1 hour). On-demand invalidation: triggered by CMS publish webhook with delivery verification (the webhook delivery failure rate should be monitored and failures should trigger an alert and a manual purge).
Zero staleness tolerance: pricing pages, availability pages, inventory counts, promotional offer expiry dates, compliance notices, and any content where a user taking an action (making a purchase, signing a contract, relying on a displayed price in a negotiation) based on the displayed value creates an obligation that the business must honor. ISR with time-based revalidation is not appropriate for this content category. Two alternatives: (1) SSR per request, serving the content fresh on every page load at the cost of server execution per request; (2) ISR with mandatory on-demand invalidation, where every content change triggers an on-demand revalidation with delivery verification before the change is considered published — the CMS update is blocked until the ISR cache has been invalidated and the fresh content is confirmed served by the CDN. The feature flag decision record documents a related pattern: feature flags that control pricing tiers or promotional offers must not be evaluated at ISR render time without on-demand invalidation on flag changes, because the flag value baked into the ISR HTML will remain stale for the full revalidation window after the flag is toggled.
Specify the on-demand invalidation implementation: Next.js provides revalidatePath(path) and revalidateTag(tag) API endpoints that can be called from a webhook handler to invalidate specific cached pages immediately. The webhook must be authenticated (bearer token or HMAC signature verification on the webhook payload) and must return an error if the revalidation fails, so the CMS can retry. Specify the retry policy for webhook delivery: a minimum of 3 retry attempts with exponential backoff, with a final fallback alert to an on-call channel if all retries fail. For zero-staleness-tolerance content, the content management workflow must be blocked until ISR invalidation is confirmed — the CMS must not mark the content as published until the application's invalidation endpoint returns HTTP 200 confirming that the cache has been purged.
Section 4: Edge rendering data model — primary vs. replica, replication lag bound, and read-after-write consistency. Enumerate the data categories fetched during edge SSR renders and specify the read source for each. Data categories that require primary reads: user-specific data written by the authenticated user in the same session (preferences, profile data, recently created records, form submissions), data with a write-then-read pattern (the user submits a form, the next page render must reflect the submission), and data with a business constraint that prohibits stale reads (inventory reservations, payment confirmation states). Data categories that tolerate replica reads: aggregate data (team statistics, usage metrics, shared dashboards that the user did not personally update), historical data (past orders, historical reports), and public data that is not user-specific (product catalog, public pricing, documentation content).
Specify the replication lag bound for the replica used by each edge region. The lag bound is not the average lag — it is the P99 or P99.9 lag, the value below which 99% or 99.9% of writes are visible in the replica within that duration. A replica with an average lag of 4 seconds and a P99 lag of 15 seconds means that 1% of writes will be invisible to the replica for more than 15 seconds — users who reload the page within 15 seconds of a write will see stale data in 1% of post-write reloads. The acceptable lag bound must be specified based on the user experience requirement, not the average: if a user saving preferences expects to see their change reflected immediately on reload, the acceptable lag bound is 0 (primary read required) or the minimum delay the user experience design tolerates (which should be measured in seconds, not minutes).
Specify the read-after-write consistency mechanism for routes where replica reads are used by default but primary reads are required after a recent write. Three implementation patterns: (1) Write token cookie: on write, the application server sets a session cookie containing the write timestamp and an HMAC-authenticated token. The edge render function checks for the cookie; if present and within the lag bound window, it reads from the primary; if absent or expired, it reads from the replica. The cookie expires after the lag bound duration plus a safety margin (P99 lag × 2). (2) Globally distributed database: use a database engine with synchronous replication to the nearest read node (CockroachDB, PlanetScale, Neon geo-replication), so the nearest replica has sub-millisecond lag from the primary. The edge render reads from the nearest node, which is always current. This eliminates the read-after-write consistency problem at the cost of higher database infrastructure complexity and cost. (3) CDN bypass on write: after a write, the client receives a response with a header or cookie that causes the CDN to bypass the ISR/SSG cache for the next request. The bypassed request hits the SSR origin (or a primary-connected edge function), guaranteeing a fresh render. Document the chosen mechanism in the ADR including its failure mode: what happens if the write token cookie is dropped (the user sees stale data on the next reload), what happens if the primary connection from the edge node fails (the edge function must fall back to the replica or return a 503), and what the operational cost of the primary read path is at the tail of the traffic distribution (high-write-rate users pay the cross-region read latency on every page load if their write tokens are continuously renewed). The cookie and session management decision record documents the security requirements for session cookies used in read-after-write consistency mechanisms: the HMAC token must use a server-side secret key, the cookie must be HttpOnly and Secure, and the cookie's domain and path scope must be restricted to prevent the token from being sent to third-party origins loaded by the page.
Section 5: Rendering observability — TTFB by region, hydration mismatch rate, ISR cache hit rate, and stale content detection. Specify the metrics required to make the rendering model observable. Four categories of rendering metrics are relevant to different failure modes.
Time-to-first-byte by region: instrument TTFB for each route category, segmented by the user's region (inferred from the edge node that served the request or from the request's IP geolocation). Alert when the P75 TTFB for any region exceeds the acceptable threshold specified in the ADR's performance contract. For edge SSR routes, TTFB should be uniformly low across regions — a spike in a specific region indicates a failure in the edge node or an edge-specific data fetch timeout. For origin SSR routes, TTFB varies predictably by region distance; an anomalous spike indicates a server-side performance regression or a database query slowdown. The observability platform decision record documents the metrics infrastructure; TTFB by region requires the edge function or CDN to emit per-request latency data including the serving region and route identifier.
Hydration mismatch rate: instrument the rate at which React's onRecoverableError callback fires with a hydration error. A nonzero rate on statically generated or server-rendered pages (where the server environment is deterministic) indicates a component reading a client-only source during the initial render. Report hydration errors to the error tracking system with the component stack trace to identify the source of the mismatch. Alert when the hydration mismatch rate on a specific route exceeds a threshold (e.g., 0.5% of page loads). A sudden increase in hydration mismatch rate after a deploy indicates a regression introduced in that deploy — a new component reading Date.now(), a new dependency that calls window during module initialization, or a new use of localStorage in the render path. The deployment strategy decision record documents the canary deployment model; hydration mismatch rate is a post-deploy canary metric: a canary deployment that shows elevated hydration mismatch rates should be rolled back before full traffic is shifted.
ISR cache hit rate: for ISR routes, instrument the fraction of requests served from the CDN cache versus the fraction that triggered a background revalidation or on-demand invalidation. A low cache hit rate on an ISR route indicates that the revalidation interval is shorter than the inter-request interval — the cache expires between requests, so every request triggers a revalidation, and the ISR model provides no caching benefit over SSR. A cache hit rate near 100% on a zero-staleness-tolerance route indicates that on-demand invalidation is not firing correctly — content changes are not reaching the CDN cache, so the cached page is served indefinitely without revalidation. Alert when the cache hit rate on a zero-staleness-tolerance ISR route exceeds 99.9% without a confirmed content change in the last 10 minutes — this indicates the on-demand invalidation webhook is failing silently.
Stale content detection: for routes where staleness has a business consequence (pricing pages, availability pages), implement active staleness monitoring: a synthetic probe that fetches the rendered page at 1-minute intervals, compares the displayed value against the live data source value, and alerts when they differ by more than the documented acceptable staleness window. The probe does not require JavaScript execution — it fetches the raw HTML and extracts the displayed value using a CSS selector or regex match against the server-rendered markup. A pricing page staleness monitor fetches the page HTML, extracts the Enterprise plan price, compares against the live pricing API's Enterprise plan price, and alerts if they differ. This monitoring closes the observability gap that ISR creates: the CDN cache serves outdated content silently, with no error response and no log entry indicating staleness, until the synthetic monitor detects the mismatch. The API contract testing decision record documents the contract testing model for ISR routes: the contract test verifies that the on-demand invalidation endpoint is correctly registered, that a CMS update triggers invalidation within the specified window, and that the post-invalidation render reflects the updated content — the test that would have caught the missing webhook handler before the pricing contract dispute.
Further reading
- The caching strategy decision record — ISR is a caching strategy applied at the render layer; the acceptable staleness window per content category is the same decision as the cache TTL per content category, and the same on-demand invalidation patterns apply.
- The CDN decision record — CDN caching and ISR/SSG are coupled: the CDN serves the pre-rendered HTML, and CDN cache invalidation is the mechanism through which on-demand ISR revalidation removes stale content from the edge; the CDN and rendering models must be designed together.
- The database read replica decision record — edge rendering's data consistency problem is a specific instance of the read replica consistency problem; the replication lag bound, acceptable staleness window, and read-after-write consistency mechanisms documented in the replica decision record apply directly to edge SSR data fetches.
- The state management decision record — components that use suppressHydrationWarning to avoid hydration mismatches must implement client-side revalidation (SWR, React Query) to replace stale server-rendered values; the state management model determines how and when client-side fetches replace server-rendered state after hydration.
- The feature flag decision record — feature flags that control pricing tiers or promotional visibility must not be evaluated at ISR render time without on-demand invalidation on flag changes; the flag value baked into ISR HTML remains stale for the full revalidation window after a flag toggle.
- The cookie and session management decision record — read-after-write consistency mechanisms for edge SSR use session cookies to carry write tokens; the cookie must be HttpOnly, Secure, and scoped to the application's domain, and the HMAC signing key must be stored in the edge function's secret store, not in environment variables baked into the deployment bundle.
- The deployment strategy decision record — hydration mismatch rate is a canary deployment rollback signal; a rendering model change is a high-risk deployment that should use a canary or blue-green deployment strategy with explicit hydration mismatch rate monitoring before full traffic shift.
- The frontend framework decision record — the framework choice constrains the rendering model options; Next.js, Remix, Astro, and Nuxt each support different rendering modes for different route types, and the rendering strategy ADR must be read alongside the framework selection ADR to understand which rendering models are available.
- The observability strategy decision record — TTFB by region, hydration mismatch error rate, ISR cache hit rate, and stale content detection are rendering-layer metrics that must be specified in the observability plan alongside application-layer metrics; the rendering observability gaps (ISR staleness is not an error, hydration mismatches are not 5xx responses) require synthetic probes and client-side error tracking to close.
- WhyChose decision extractor — finds the founding rendering sessions in your ChatGPT or Claude export — the "how should we structure the Next.js app?" conversation, the "should the pricing page use ISR?" thread, the "let's move the dashboard to edge functions" planning session — and extracts the decision and the trade-offs that were actually weighed, without the surrounding framework comparison that buries the rendering model choice in forty messages about bundle size and deployment configuration.
Frequently asked questions
When does client-side rendering cause SEO failures, and how do you detect that a search engine is not indexing your page content?
Client-side rendering causes SEO failures when the search engine crawler indexes the initial HTTP response — which contains only the HTML shell and no page content — rather than the JavaScript-rendered output. Googlebot operates two indexing passes: a primary crawl that processes raw HTML without JavaScript execution, and a secondary rendering queue that executes JavaScript for some pages. Pages not selected for JavaScript rendering are indexed with only the shell content. The practical detection method is Google Search Console's URL Inspection tool: compare the Rendered HTML (what Googlebot produced) against the Live Test (what a browser renders). If the Rendered HTML shows only the root div and not the page content, the content is not indexed. A second method: query site:yourdomain.com and inspect the SERP snippets — if snippets show no page-specific content (only title and meta description), the body content was not indexed. A third method: fetch the page with curl and check whether the page content appears in the raw HTML — if the HTML contains no product names or prices, a JavaScript-free crawler sees the same empty shell.
What is the acceptable staleness window for ISR revalidation, and which types of content cannot tolerate hourly staleness?
The acceptable staleness window depends on whether a user can take a financial, legal, or contractual action based on the stale content. Content that tolerates hourly or daily staleness: blog posts, documentation, team pages, marketing feature descriptions — the consequence of seeing outdated content is a poor user experience, not an obligation. Content that cannot tolerate staleness beyond seconds or minutes: pricing pages where a displayed price creates a customer expectation the business must honor, availability or inventory pages where "in stock" drives a purchase decision, promotional pages where a displayed discount creates a commitment, and legal notices where the current version must be shown. For zero-staleness-tolerance content, ISR with time-based revalidation is insufficient — on-demand invalidation with delivery verification is required, or SSR per request. The content management workflow must confirm that the ISR cache is invalidated before marking a content change as published.
How do you specify the database read model for edge-rendered pages to prevent replication lag from causing stale rendered content?
Edge-rendered pages that display data written by the same user in the same session must read from the primary database to prevent replication lag from rendering stale content. The ADR must enumerate which data categories require primary reads and which tolerate replica reads, and specify the read-after-write consistency mechanism for routes where replica reads are the default. Three practical patterns: (1) Write token cookie — the application sets a session cookie with an HMAC-authenticated write timestamp on every write; the edge function reads from the primary only if the cookie indicates a write within the replica lag bound window. (2) Globally distributed database — use a database engine with synchronous or near-synchronous geo-replication (sub-millisecond replica lag) so replica reads are always current. (3) CDN bypass on write — a cookie or header set after a write causes the CDN to bypass the cached SSR response for the next request, routing it to an origin that reads from the primary. Each pattern must document its failure mode: what happens when the write token cookie is dropped, when the primary connection from the edge node fails, and what the operational cost is for high-write-rate users who continuously renew their write tokens.