Setting up from the UK or Europe? Compare UAE free zones for 2026 in our setup guide.

Read the guide
Blog · DevOps

Caching a Next.js Site on Cloudflare Without Breaking It

CSIRO ScienceImage 2042 A row of computer servers in a server rack

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 Vary on rsc and the router headers, and adds an _rsc query parameter as a cache-key discriminator for CDNs that ignore Vary. Stripping query strings from the cache key breaks navigation.
  • revalidateTag() and revalidatePath() 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).

Questions readers ask

Do I need Cloudflare at all if my host is already fast?

If your origin is in the UAE and your audience is in the UAE, the distance saving is small and the benefit is mostly about absorbing traffic spikes and not recomputing the same page. The larger win for most Dubai sites is making routes static in the first place. A CDN in front of a dynamically rendered page changes very little.

Why does my site work fine until someone clicks an internal link?

That is the classic symptom of the RSC variant being mishandled. The first load is HTML and works; the client-side navigation asks for the RSC payload, gets HTML from the cache, and the router cannot parse it. Check that the rsc header reaches your origin and that _rsc is in the cache key.

Is it safe to ignore the origin cache-control header and set my own TTL?

It is available and occasionally necessary, for example in front of a legacy origin that sends nothing useful. For a Next.js app it is usually the wrong call, because you are overriding a framework that already distinguishes static, revalidating and dynamic routes per page. If you do override, do it on a narrow path match rather than site-wide.

How long should the edge TTL be?

Let the origin decide it through s-maxage , and set the revalidate window in the application where the content team's expectations live. If marketing expects an edit to appear within the hour, that is a one hour revalidate plus a purge call on publish, not a guessed CDN number.

Does this apply to the Pages Router as well?

The RSC and router header machinery is App Router behaviour. A Pages Router site has a simpler caching story, with getStaticProps and ISR producing plain HTML and JSON that cache on the URL. If you are migrating, the cache rules need revisiting rather than carrying over.

What breaks first when a cache rule is wrong?

In our experience, in this order: client-side navigation on the App Router, then stale content after a CMS publish, then personalised or logged-in pages served to the wrong person. The third is the one that matters, which is why the bypass rules for authenticated paths go in before the caching rules go live rather than after.

Keep reading

Want this done for your company?

Tell us what you are launching and we will come back with a written quote.

Get a free quote