Cache-Control and CDN: the caching strategy for a Next.js landing page on Vercel
Published on 1 September 2026 · 8 min read
A landing page that responds in 40ms usually hasn't run anything: it was simply served from a cache somewhere between the visitor's browser and the origin server. That's the most effective way to cut down server response time (TTFB) — faster than optimizing code, since there's simply no code left to run. But "caching" actually covers three distinct mechanisms, each with its own rules and its own risks. Mixing them up produces two opposite symptoms: a cache that does nothing because it's bypassed on every request, or a cache that serves the wrong response to the wrong person.
Three caches, three jobs
Between an ad click and the landing page rendering, an HTML response can pass through up to three successive cache layers, each with its own lifetime and its own invalidation trigger.
- Browser cache — stored on the visitor's device, it skips a full network request on a repeat visit. Controlled by the
Cache-Controlheader returned by the server (max-age). - CDN cache (Vercel Edge Network) — a copy of the response placed on geographically distributed servers, between the browser and the server function. It's what lets a visitor in Lyon receive the page from Paris instead of the region where the app is actually deployed. Controlled by the
s-maxagedirective, distinct frommax-age. - Next.js's internal cache — the Full Route Cache (the HTML and React payload already generated for a static route) and the Data Cache (the result of a server-side
fetch). This layer decides whether a page is regenerated on every request or served as-is from the last build.
These three caches share neither the same lifetime nor the same invalidation mechanism: clearing the CDN cache doesn't clear a visitor's browser cache if they already have the page stored locally, and redeploying on Vercel doesn't immediately force a refresh of a browser cache set for several hours. A coherent caching strategy sets all three deliberately, not just one at random.
What a static landing page already caches, by default
A landing page generated with generateStaticParams — the case for every /templates/[slug] or /blog/[slug] page on this site — is produced once at build time, then served as-is: HTML already written, no server function to run on request. On Vercel, that static output is automatically placed on the CDN with a Cache-Control geared toward a long edge cache, invalidated only by a new deployment. That's the fastest configuration possible, and for a page whose content only changes when someone actually edits it, the most sensible one: nothing justifies regenerating a blog post on every single visit.
ISR: revalidating without rebuilding the whole site
For data that changes between two deployments — a price, stock levels, a counter — incremental static regeneration (ISR) is still the recommended approach: the page keeps being served from cache until its revalidate window expires, then Next.js rebuilds it in the background on the next request, without ever rebuilding the whole site. The article on ISR and SSG for a price that changes covers this mechanism in depth, along with on-demand revalidation (revalidateTag); the key point here is that ISR is, at every moment, still a cached response — never a real-time computation.
Pitfall #1: personalization breaks the cache — hiding it makes it worse
The problem isn't personalization itself, it's where it happens. A landing page that computes a different offer based on a cookie, an IP-based geolocation, or a server-side session can no longer be served from a plain CDN cache: every visitor would need a different response for the same URL, which a CDN can't handle without specific configuration (a Vary header, cache keyed by segment). There are two possible outcomes, equally bad: either the cache is disabled and every visit wakes up a server function — slower, more expensive in invocations — or, worse, the response computed for the first visitor in a cache window gets served as-is to everyone after them, expired offer included, until the cache expires. That's exactly the risk avoided by how this site computes its template of the week offer: the offer-rotation engine (lib/promo.ts) is fully deterministic from the date, but the computation happens client-side, at component mount — never in server rendering. The HTML page stays 100% static and cacheable at the edge without limit; it's the browser, once the page has loaded, that displays the current offer. The countdown is real, never false scarcity, and the cache never has to choose between speed and freshness.
Pitfall #2: a misconfigured cache can leak one visitor's response to another
The reverse risk, more serious, is documented under the name web cache deception: a CDN caches a response meant to stay private — because a path confusion (a static-looking file extension appended to a dynamic route, for instance) makes it look like a public, cacheable resource — then serves that cached response to any later visitor who requests the same URL. A study by Mirheidari et al. ("Cached and Confused: Web Cache Deception in the Wild") measured this vulnerability at scale and found it exploitable on a significant share of tested sites, including major platforms. On a landing page, the exposure surface is small — no user accounts — but it exists as soon as a route returns a response tied to a session or a token, like /api/download with its download token: those routes must explicitly return Cache-Control: private, no-store and should never be left at the CDN's default setting.
Cache-Control cheat sheet
| Directive | Role | Example use |
|---|---|---|
public | Allows caching by a shared cache (CDN) | Static page, image, font |
max-age | Freshness window for the browser cache | max-age=3600 on an OG image |
s-maxage | Freshness window for the CDN cache, takes priority over max-age | s-maxage=31536000 on a built static page |
stale-while-revalidate | Serves a stale version while revalidating in the background | The typical case for ISR under Next.js |
private | Blocks any shared cache, allows only the browser | A route tied to a session |
no-store | Blocks caching at every layer | /api/download, a form with a token |
Why it matters: perceived speed has thresholds, not a smooth scale
The point of caching isn't just the number shown in a measurement tool: it's crossing psychological thresholds documented for over half a century. In a foundational 1968 study still widely cited in software ergonomics, Robert B. Miller ("Response Time in Man-Computer Conversational Transactions") identifies three tiers: under 0.1 second, a response is perceived as instantaneous; under one second, a sequence of actions still feels continuous, with no break in concentration; past ten seconds, attention drifts away. A response served from Vercel's CDN cache is measured in tens of milliseconds; a response that has to go through a server function, a cold DNS lookup, and a TLS handshake can easily exceed a second on a first load. That's not a cosmetic nuance: it's the difference between a page that "responds" and a page that "loads."
In practice, on Vercel
- Leave static pages (
/templates/[slug],/blog/[slug]) without arevalidateor a call tocookies()/headers()in rendering: Next.js treats them as fully static, and Vercel places them on the edge without a duration limit, invalidated on the next deployment. - Add a
revalidatewindow (in seconds) only where data actually changes between two deployments — never "just in case" on a page that never moves, which would only reintroduce an unnecessary server round trip. - Keep any personalization that genuinely depends on the visitor (offer, detected language, login state) client-side, after the first render — not in server rendering for a page meant to stay static.
- Explicitly set
Cache-Control: private, no-storeon any route that returns data tied to a session, a token, or an email address — never left to a shared cache's default behavior.
A good caching strategy is never directly visible in a landing page's design — it shows up in load time, in the hosting bill, and in the total absence of a "wrong person saw the wrong offer" incident. It's the kind of technical detail that converts nobody on its own, but quietly shapes every metric that does. LanderKit templates (€89 each, €229 for the full pack of 10) ship as static Next.js projects by default, built to be deployed on Vercel with no cache configuration to improvise — one-off personalization (countdown, offer of the week) always stays client-side, so you never have to choose between a fast cache and a correct one.
FAQ
Frequently asked questions
What's the difference between max-age and s-maxage?
max-age sets the freshness window for the visitor's browser cache. s-maxage does the same for a shared cache like a CDN, and takes priority over max-age for those caches: you can, for instance, keep a page cached for a long time at the CDN while forcing more frequent revalidation on the browser side.
Is a static Next.js landing page automatically cached on Vercel?
Yes, as long as the page doesn't call cookies(), headers(), or per-request dynamic data in its server rendering: Next.js treats it as static, and Vercel serves it from its CDN network without running a server function, until the next deployment or a configured revalidation.
Why not just personalize every page server-side for each visitor?
Because that makes the page impossible to cache normally at the edge: either every visit triggers a server function (slower, more expensive), or the first computed response gets cached and wrongly served to every later visitor. Computing personalization client-side after the first render avoids that trade-off.
What is web cache deception, and is a landing page exposed to it?
It's a flaw where a CDN mistakenly caches a response meant to stay private, then serves it to other visitors. A typical landing page has little exposure with no user accounts, but any route tied to a token (like a download link) should explicitly set Cache-Control: private, no-store to stay out of reach of a shared cache.
Read next
Related articles
- IP geolocation on a landing page: personalizing without breaking SEO or spooking visitorsDetecting a visitor's city from their IP address and adjusting the headline, currency, or a local testimonial accordingly sounds appealing — but IP geolocation is less accurate than assumed, and personalized carelessly it can start to look like cloaking to Google. What this technique actually delivers, how to wire it up cleanly in Next.js on Vercel, and where to stop.
- From Figma to a Next.js landing page: what always gets lost in translationA mockup approved in a meeting, then a landing page shipped three weeks later that no longer quite matches: spacing that crept tighter, a fallback font flashing for an extra beat, a button that doesn't respond the way it did in the prototype. This isn't a one-off slip — it's a gap measured in design-work research, and there are concrete ways to close it.
- Deploying your Next.js landing page on Vercel: the step-by-step guideA purchased Next.js template converts nobody until it's live. Step-by-step guide to deploying it for free on Vercel: Git repository, first deployment, custom domain and automatic updates.