Putting a Next.js App Router site behind Cloudflare speeds it up only if the cache key accounts for the fact that the same URL returns two different documents. Ask for a page normally and you get HTML. Ask for it with an RSC request header and you get a React Server Components payload with the content type text/x-component. A cache rule that keys on the URL alone will eventually hand one to a visitor who asked for the other, and the symptom is a blank page or a client-side navigation that reloads the whole browser tab.
Key takeaways
- Check what your origin already sends before writing any rule. A dynamically rendered Next.js page sends
Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate, and no CDN will cache that. - Next.js sets
Varyonrscand the router headers, and adds an_rscquery parameter as a cache-key discriminator for CDNs that ignoreVary. Stripping query strings from the cache key breaks navigation. revalidateTag()andrevalidatePath()clear the Next.js cache, not the CDN. The edge keeps serving its copy until the TTL expires unless you also call the purge API.- A custom cache key that includes headers or cookies stops single-file purge working from the Cloudflare dashboard, which is a real operational cost.
- Two curl commands tell you whether your site is cacheable at all, and they take about thirty seconds.
Start by reading what your origin sends
Most teams reach for a cache rule before they have looked at the response headers. That order wastes an afternoon. Next.js already tells you what it thinks each route is, and the official CDN caching guide, current at version 16.3.5 and last updated on 25 August 2026, documents exactly which header goes with which rendering strategy.
- Static pages with no revalidation:
s-maxage=31536000, one year. - Pages using time-based revalidation:
s-maxage={revalidate}, stale-while-revalidate={expire - revalidate}, with a default expiry of one year. - Dynamic pages:
private, no-cache, no-store, max-age=0, must-revalidate. - Files under
/_next/static/:public, max-age=31536000, immutable, because the filenames carry content hashes.
Here is a live example rather than a hypothetical. Running curl -sSI https://codeeo.com/service/web-design/ on 18 September 2026 returns Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate together with Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Accept-Encoding, while the stylesheet under /_next/static/css/ returns public, max-age=31536000, immutable. That is the signature of a route being rendered dynamically on every request. Two loads measured 1.3 and 2.5 seconds to first byte from the same connection. No cache rule fixes that page, because the page is telling every cache in the chain not to store it. The work is in the application: make the route static or give it a revalidate window, and only then put a CDN in front of it.
The two documents behind one URL
This is the part that causes the strange bugs, and it is easy to see for yourself.
Request a page normally and you get HTML. Request the same URL with -H "RSC: 1" and the response comes back as Content-Type: text/x-component, beginning with a numbered flight payload rather than a doctype. Both responses are correct. They are what the App Router needs for a full page load and for a client-side navigation respectively.
Next.js signals the difference with a Vary header. The guide lists the request headers that responses can vary on: rsc, which decides RSC payload or HTML; next-router-state-tree, the client’s current router state; next-router-prefetch; next-router-segment-prefetch; and next-url, added only for routes that use intercepting routes.
Plenty of CDNs do not honour Vary on arbitrary headers without configuration, so Next.js also appends an _rsc search parameter that hashes the relevant header values. That parameter is the cache key discriminator, and the documentation is blunt about what happens if you drop it. The rsc header must be forwarded to the origin: strip it and the server returns HTML when the router expected RSC data, which breaks client-side navigation and forces browser navigations instead. The _rsc parameter must be part of the cache key, and some CDNs strip query strings from cache keys by default.
There is one more behaviour to plan around. When an RSC request arrives without the correct _rsc value, the server responds by default with a 307 redirect to the URL carrying the right hash, which your CDN needs to follow. That behaviour can be turned off with experimental.validateRSCRequestHeaders set to false, but turning it off without understanding why it exists usually trades a redirect for a wrong response.
Writing the Cloudflare rules
With the origin sending cacheable headers, the Cloudflare side is short. Cloudflare’s cache rules settings documentation, last updated on 16 September 2026, covers each control.
Edge TTL offers three behaviours: use the origin’s cache-control header if present and bypass if not, use it if present and fall back to default Cloudflare behaviour if not, or ignore the origin header entirely and apply a TTL you set. For a Next.js site, respecting the origin is the right default, because the application already encodes its own rendering strategy in s-maxage. Overriding it means the CDN and the framework disagree, and the framework is the one that knows which routes cannot be cached.
The cache key controls are where the RSC problem is solved or created. Query string handling can include all parameters, all except a named set, only a named set, or ignore them. Do not choose an option that drops _rsc. If you are trimming tracking parameters from the cache key to improve the hit rate, which is a reasonable thing to want, use the “all except” form and exclude the analytics parameters by name rather than the “only” form, so that _rsc survives changes to the framework.
Cloudflare also exposes vary header handling, cache by device type, sort query string, serve stale while revalidating and cache deception armor as rule settings. Serve stale is worth enabling for marketing pages; it pairs naturally with the stale-while-revalidate directive Next.js already sends on revalidating routes.
One trade-off deserves stating before you build anything clever. Cloudflare documents that custom cache keys including headers, cookies or other request properties prevent single-file purge from working from the dashboard, because the dashboard cannot send those values in a purge request. If your content team purges individual URLs after edits, a header-based cache key takes that ability away from them. Rule counts are also plan-limited: ten cache rules on Free, twenty-five on Pro, fifty on Business and three hundred on Enterprise.
Purge discipline after a content edit
This is the failure that embarrasses people in front of clients. A page is updated in the CMS, the editor refreshes, and the old version is still there.
The Next.js guide states the mechanism plainly: CDN-level caching alone does not support on-demand revalidation. Calling revalidateTag() or revalidatePath() invalidates the Next.js server cache, and the CDN carries on serving its cached copy until s-maxage expires. The documented pattern is to invalidate the framework cache and then call the CDN purge API for the affected keys, including both the HTML and the RSC variants of the URL.
That last clause is the one teams forget. Purging /services/web-design/ and stopping there leaves the RSC variant in the edge cache, so a visitor arriving through a link sees the new page while a visitor navigating from another page on the site sees the old one, which is a confusing bug to receive as a report. Build the purge into the publish step so it is not a manual habit that depends on someone remembering.
A related note from the same guide: proxy.js, previously called Middleware, should run before the CDN cache so it stays the source of truth for authentication, redirects and rewrites. If your deployment puts it behind the CDN, configure the cache to bypass any route whose behaviour depends on it.
What to cache and what to leave alone
A workable default for a Dubai business site, whether it is a services site, a property portal or a content-heavy blog, looks like this.
| Path | Treatment | Reason |
|---|---|---|
/_next/static/* |
Cache, long TTL, respect origin | Content-hashed filenames, safe to keep for a year |
| Images and fonts | Cache, long TTL | Rarely change, and changes get new filenames |
| Marketing and blog routes | Cache, respect origin, serve stale | Static or revalidating, and the framework already says which |
| Search and filtered listings | Bypass unless you have counted the parameter combinations | Query permutations fragment the cache and can lower the hit rate |
| Anything behind a login | Bypass | Per-user responses, and a mistake here leaks another user’s data |
| API and form endpoints | Bypass | Writes must reach the origin |
One practical caveat for paid traffic. Ad platforms append click identifiers such as gclid to landing page URLs, so if your cache key includes all query parameters, every ad click is a cache miss and arrives at origin speed. That is the traffic you are paying for, which makes it the worst traffic to serve slowly. Excluding the click identifiers from the cache key is the fix, and it only works if you excluded them by name rather than switching to an allowlist that also drops _rsc.
Measuring the change honestly
Time to first byte is the number that moves, and it is worth measuring properly rather than reloading the page and forming an impression.
curl -sS -o /dev/null -w "ttfb=%{time_starttransfer}sn" https://example.com/page/ gives a single measurement. Run it five or six times, discard the first, and compare against the same command before the change. Add -I and look for cf-cache-status: HIT means the edge served it, MISS means it went to origin and was stored, DYNAMIC means Cloudflare decided the response was not cacheable, which usually points back at the origin’s cache-control header rather than at your rule.
Field data matters more than any of this. Server response time feeds Largest Contentful Paint, and Google assesses Core Web Vitals at the 75th percentile of page loads with a good threshold of 2.5 seconds for LCP. A caching change that improves your own curl timings from a fast connection in one city can do less than expected for real users, which is why the Chrome User Experience Report reading over the following month is the one to check. Our guide to diagnosing INP covers the interaction side of the same measurement problem.
If you want the short version: read your headers, make the routes static before you reach for a CDN, keep _rsc in the cache key, and purge both variants when content changes. Our team works on web application development and the hosting and delivery setup behind it, and the caching decisions are usually cheaper to make before a site launches than after. If you are still choosing the stack that sits behind all of this, our comparison of WordPress and Strapi for Dubai teams covers the CMS side.
Header values, documentation dates and the live measurements above were checked on 18 September 2026 against the Next.js and Cloudflare documentation and against the site named. Framework behaviour and CDN settings change between versions, so re-read the current docs before you apply any of this. Cover photo: A row of computer servers in a server rack by CSIRO, via Wikimedia Commons (CC BY 3.0).





