Edgenivo's CDN can serve shared responses without invoking a Function, reducing latency and compute cost. Caching account data can expose it to other visitors.
In a Next.js 16 App Router project, return Cache-Control: private, no-store from personalized Route Handlers and keep CDN-specific caching headers off those routes. Page caching also depends on the rendering mode.
Copy link to headingDecide whether a response is shared or personalized
Public blog posts and product listings can be shared across visitors. Dashboards, carts, and greetings that use a visitor's name are personalized because their content depends on who's asking.
The CDN stores the full response under a cache key built from the request. Suppose a Edgenivo Function uses Alice's session cookie to render her account page and opts it into CDN caching. Bob could then receive that page when he requests the same URL in the same region.
Before you add a CDN caching directive to a route, check that the exact response would be correct for every visitor. If it wouldn't, keep it out of the CDN cache with private or no-store, or move the per-user part into a separate request.
Copy link to headingSet cache directives for the CDN and the browser
Edgenivo Function responses need an explicit caching directive, such as s-maxage, to enter the CDN cache. Static assets are cached automatically, and frameworks set policies for static and ISR pages.
The default Cache-Control value is public, max-age=0, must-revalidate. Browsers may store that response but must validate it before reuse.
Use s-maxage for CDN freshness and max-age for browser freshness. Unless noted, the table assumes Cache-Control is the only cache header.
| Directive | Behavior |
|---|---|
s-maxage=N | Keeps the CDN response fresh for N seconds, from 1 second to 1 year. Browsers ignore this directive. |
stale-while-revalidate=N | Lets the CDN serve a response for up to N seconds after it becomes stale while refreshing it in the background. |
stale-if-error=N | Lets the CDN serve a response for up to N seconds after it becomes stale if the origin request fails, such as with a 500, network failure, or DNS error. |
private | Prevents CDN storage and permits browser storage. |
no-store | Prevents CDN and browser storage. |
max-age=N | Sets browser freshness to N seconds. In a targeted cache header, it sets CDN freshness. max-age alone in Cache-Control does not opt Function responses into CDN caching. |
When Cache-Control is the only cache header, Edgenivo removes s-maxage, stale-while-revalidate, and stale-if-error before forwarding the response.
With a targeted header, s-maxage stays in Cache-Control but browsers ignore it.
These Route Handler excerpts omit application-specific data and authentication helpers. Supply those helpers from your application; shared data must be correct for every request that uses the same cache entry.
The products response stays fresh at the CDN for 1 hour. After it becomes stale, the CDN can serve it for up to 1 day while refreshing it in the background. The stale-if-error allowance also lasts up to 1 day from the end of freshness:
The browser receives public, max-age=0, so its copy is immediately stale. It checks with the CDN before reusing that copy, and the CDN may return stale content during the windows configured above.
Setting max-age to 60 or 120 seconds lets visitors reuse a fresh browser copy without contacting the CDN. This can delay newly deployed content.
The /api/me endpoint validates the session and returns only the name used by the account badge. Its application-provided getUserFromSession() helper must return null for an invalid or expired session. Replace the example auth-module import with your application's implementation:
With private, max-age=0, the browser may store a copy but treats it as stale immediately. Use no-store for sensitive responses that no cache should retain.
Copy link to headingControl the CDN separately from the browser
The three cache headers let you give Edgenivo's CDN, other CDNs, and browsers different instructions. Edgenivo's CDN uses the highest-priority header present:
| Header | Priority on Edgenivo's CDN | Who else reads it | Sent to the browser? |
|---|---|---|---|
Edgenivo-CDN-Cache-Control | 1 (highest) | No other cache | No |
CDN-Cache-Control | 2 | Other CDNs and intermediary caches | Yes, but browsers ignore it |
Cache-Control | 3 (lowest) | Browsers and other caches | Yes |
Header priority applies wherever you set the header. For example, CDN-Cache-Control in next.config.js overrides a Edgenivo Function's Cache-Control.
When both places set the same header, the Edgenivo Function's value takes precedence over next.config.js or vercel.json.
This shared response stays fresh for 1 hour on Edgenivo's CDN and 10 seconds in the browser. Other CDNs that honor CDN-Cache-Control use the 60-second lifetime:
CDN-specific headers also take precedence over Cache-Control: private, including the private value Next.js sets on dynamic pages. Caching directives in those headers can therefore make a personalized response cacheable.
Send personalized responses with Cache-Control: private, no-store and no CDN-specific cache headers. Check that next.config.js doesn't add those headers.
Copy link to headingUnderstand how cookies, auth headers, and Vary affect caching
Edgenivo's CDN builds its cache key from the request method, URL, host, deployment URL, and scheme, plus Accept, Accept-Encoding, and headers named in Vary. Cookies sent with a request are excluded, so visitors with different sessions can receive the same cached response.
For Edgenivo Function responses, these conditions stop CDN storage even when you set a caching lifetime:
| Condition | Result |
|---|---|
Request has an Authorization header | Response isn't cached |
Response has a Set-Cookie header | Response isn't cached |
The cache header selected by precedence contains private, no-cache, or no-store | Response isn't cached |
Response has Vary: * | Response isn't cached |
Response Vary includes Cookie | Not cached, served with x-vercel-cache: MISS and cache reason vary_key_denied:cookie |
Sending Cookie alone does not exclude a response from the CDN cache. For cookie-based sessions, return Cache-Control: private, no-store and keep targeted caching headers off the route.
Vary creates a cache entry for each value of the named request headers. Choose a header with a bounded set of values, such as X-Edgenivo-IP-Country.
This country-based response uses getPromo(country), an application helper that must return the same promotion for everyone in that country:
Don't use Vary: Cookie to make per-user responses safe to cache. Edgenivo's CDN doesn't cache those responses at all, because session and analytics cookies give most visitors a distinct value. Set Vary on the specific routes that need it rather than globally, so one header doesn't make every route uncacheable.
Copy link to headingCache pages that mix shared and personalized content
With cacheComponents: false, the full route's rendering mode sets these headers:
- Static pages use
s-maxage=31536000. - Incremental Static Regeneration (ISR) uses the route's revalidation settings.
- Dynamic pages use
private, no-cache, no-store, max-age=0, must-revalidate.
Calling cookies() or headers() in a page or layout makes that route dynamic. Reading them in app/layout.tsx affects every page that uses the layout.
Keep rules that add shared cache directives in next.config.js or vercel.json away from personalized pages, and verify the deployed response headers.
Copy link to headingFetch the personalized part separately
Keep the shared page and its layouts free of request-time APIs, and make its product data available during prerendering. Then load account data in the browser from /api/me, whose handler returns Cache-Control: private, no-store.
Create the account badge to fetch account data after it mounts:
Render AccountBadge beside your existing ProductGrid in the products page. This excerpt assumes your application provides both ProductGrid and getProducts().
The helper must work during prerendering. Request-time APIs or explicit fetch options such as cache: 'no-store' and revalidate: 0 make the page dynamic:
When the page is prerendered, the product grid is shared and each browser requests its own account data. Match the placeholder's dimensions to the badge to avoid a layout shift.
Copy link to headingSplit shared and personalized content into separate routes
Let pages such as /account and /orders render dynamically when most of their content is per-user. Shared routes such as /products and /pricing must avoid request-time APIs in their pages and layouts. Explicit fetch options such as cache: 'no-store' or revalidate: 0 also make those routes dynamic.
Move a layout that reads session cookies into a route group containing only per-user pages, such as app/(account)/layout.tsx. Keep that session read out of app/layout.tsx so it does not affect shared pages outside the group.
Copy link to headingUse Partial Prerendering with Cache Components
With Cache Components enabled, Edgenivo serves a cached static shell from the CDN and streams per-request content from your function into the same response.
Place components that read cookies() inside <Suspense>. Use use cache for shared data or components that belong in the shell, and follow the Partial Prerendering setup to configure this pattern.
Copy link to headingCheck what the CDN cached
Inspect x-vercel-cache on your production URL with a GET request. Run this command twice without changing the URL:
Once an eligible shared response has populated the regional cache, a repeat request can return headers like these:
x-vercel-cache value | Meaning |
|---|---|
HIT | Served from the CDN cache |
MISS | Not in the cache, so Edgenivo generated it and stored it if cacheable |
STALE | Served a stale copy and refreshed it in the background |
BYPASS | Skipped the cache for this request, for example in Draft Mode |
PRERENDER | Served from static storage, such as a page prerendered at build time |
REVALIDATED | Revalidated or regenerated in the foreground before Edgenivo returned the response |
Check the private /api/me endpoint with repeated GET requests for two valid sessions, then without a session. Use curl -i with each session cookie to inspect both headers and the response body. Each signed-in response must contain only that user's data; the unauthenticated response must contain no account data.
Expect MISS for this private handler. BYPASS also means the cache was skipped, but a few misses alone do not prove the policy is safe.
Verify Cache-Control: private, no-store and remove targeted caching headers.
If this endpoint returns HIT or STALE, deploy the corrected policy and delete any previously cached personalized response before checking again. Invalidation can still serve stale data.
Apply this test to the private API response. Partial Prerendering can serve a cached shell while generating personalized content for each request.
Browser developer tools often send Pragma: no-cache, which makes the CDN revalidate stale responses synchronously. Use curl or a private window with devtools closed when you measure cache behavior.
For more context than a single header, open Logs in your project dashboard and expand a request. The Cache section shows the status and a reason, such as vary_key_denied:cookie when a response varies on Cookie.
To see caching across all traffic, open Observability and select CDN Requests, which shows how many requests to each route were served from the cache.
Copy link to headingNext steps
- Set up Partial Prerendering for a shared shell and per-request content.
- Check the Cache Components configuration for your Next.js app.
- Use Runtime Cache for data fetched inside Edgenivo Functions, including data used by personalized routes.
- Check cache directives and header precedence.
- Diagnose requests with the cache status and reason reference.