Apr 03, 2026·8 min read

Nginx Cloudflare caching: where each layer pays off

Learn Nginx Cloudflare caching on a budget: what to cache at the edge, in Nginx, or in the app, and where duplicate rules waste time.

Nginx Cloudflare caching: where each layer pays off

Why this gets messy fast

A single request can hit several caches before your app does any work. Cloudflare may answer at the edge, Nginx may keep its own copy, and the app may also cache data or full responses. Fast pages are great. Stale pages are not. Once old content shows up, teams often lose time guessing which layer served it.

Each layer sees the request differently. Cloudflare cares about the edge response. Nginx cares about what the origin returned. The app usually caches something more specific, like a query result, a rendered fragment, or a user-specific response. If one layer serves old content, the layer behind it never gets a chance to show the fresh version.

A typical request path looks like this:

  • Cloudflare checks whether it already has the page or asset.
  • Nginx checks its own cache or forwards the request.
  • The app checks memory, Redis, or database-level cached data.

That stack is common on a tight budget because each piece looks cheap and useful on its own. Trouble starts when teams add caching one step at a time without a clear plan. A page feels slow, someone adds another cache, and soon nobody knows which rule fixed anything.

Headers often make it worse. Your app may send one Cache-Control policy, Nginx may rewrite it, and Cloudflare may follow a different rule because of its own defaults. Then you purge one layer and still see the old page. After a few refreshes, a private window, and a header check, you've spent half an hour on a problem that came from overlapping rules.

Duplicate caching has a real cost. More layers mean more purges, more exceptions, and more ways to cache the wrong thing. On a small SaaS, the time spent debugging stale pages can cost more than a slightly busier origin.

Caching works best when each layer has one clear job. Once two layers try to solve the same problem, the speed gains flatten out and debugging gets much slower.

What each layer does best

Each cache layer solves a different kind of delay. When teams lump them together, they often cache the same thing three times and still end up with slow pages or stale content.

Cloudflare is best at the edge. It works well for files that many visitors request again and again, like images, scripts, stylesheets, and fonts. It can also cache some full pages when the content barely changes. Because it stays close to the visitor, it cuts travel time and absorbs traffic spikes before they hit your server.

Nginx works best at the origin. It sits in front of the app and handles HTTP behavior: headers, compression, routing, and cache rules. It can cache short-lived responses from the app, which helps with public pages and API endpoints that many users request in the same form. It also tells Cloudflare what is safe to cache by sending clear headers.

The app cache solves a different problem. It stores work that only the app understands, such as a slow database query, a computed report, a permission check, or part of a dashboard. This saves database time and CPU time. If content changes by user, account, or plan, the app usually knows that first. Cloudflare and Nginx do not.

A useful way to think about it is distance versus knowledge. Cloudflare is far from your code but close to the visitor. Nginx is close to your app and good at HTTP rules. The app knows the most about the data itself.

That split usually keeps costs down. Let Cloudflare handle repeat static traffic, let Nginx cache safe origin responses for a short time, and let the app keep expensive data warm. When each layer has one job, the system is faster and easier to debug.

Start with the request path

A good cache plan starts with a plain map of what people request. If you skip that step, you end up caching whatever feels slow instead of whatever actually costs you money.

Write down your main page types and API routes. Keep it concrete: marketing pages, docs, pricing, product images, dashboard HTML, search results, login, account settings, exports, and background API calls.

A simple split is enough:

  • public pages anyone can see
  • static files like images, JS, and CSS
  • public API responses used by many visitors
  • user pages behind login
  • user API routes with private data

Then mark how often each one changes. Some content changes once a month. Some changes every hour. Some values, like inventory, live metrics, or unread counts, may change every few seconds. That timing tells you where caching is safe and where it will just create stale content.

Make a hard split between public and private content. A blog post, help page, or product screenshot can usually sit at the edge for a while. A billing page, admin panel, or anything with a user name, account balance, or custom permissions needs more care. If the response depends on cookies, auth headers, or tenant settings, treat it as private until you prove otherwise.

Then look at what actually burns CPU time. Teams often guess wrong. A page may look large, but the expensive part is often an API route that joins several tables, renders a chart, or runs permission checks on every request. That is where caching can save real money.

A common small SaaS pattern looks like this: the homepage, docs, and assets work well at the edge; Nginx helps with compression, static files, and a few short-lived public responses; the app keeps the most context-aware cache, such as expensive query results or computed dashboard widgets. That split usually beats repeating the same cache rules in every layer.

If you can answer four questions for each request - who sees it, how often it changes, whether it is public, and what it costs to generate - the rest of the plan gets much easier.

How to place caches step by step

Start at the edge, not inside the app. In most setups, the cheapest win comes from letting Cloudflare cache static files first: CSS, JavaScript, fonts, images, and other versioned assets. Those files rarely need fresh origin work, so every edge hit saves bandwidth, CPU, and time.

Then look at origin load. If the server still spends a lot of time rendering the same public pages, add a short Nginx cache for public HTML only. Good candidates are a home page, pricing page, docs page, or blog post. Keep that cache short, often 30 to 120 seconds, so updates still show up quickly.

A simple order works well for most teams:

  1. Let Cloudflare cache static assets with long TTLs and versioned file names.
  2. Keep login pages, auth responses, dashboards, and user-specific API output out of edge cache.
  3. Add Nginx caching for public HTML only if the origin still runs hot.
  4. Cache expensive database reads inside the app with clear TTLs.
  5. Measure hit rate, response time, and server load after each change.

Step three is where many teams go wrong. They turn on Nginx cache before checking whether Cloudflare already removed most of the easy traffic. If Cloudflare handles assets well, you may not need much Nginx caching at all.

Inside the app, focus on slow reads that repeat often. That might be a pricing table, feature flags, a list of plans, or a heavy report query that many users trigger. Give each cached result a clear lifetime, like 15 seconds, 1 minute, or 5 minutes. Pick a number that matches how often the data really changes.

A simple SaaS example makes this clearer. Let Cloudflare serve landing page assets. Let Nginx hold public marketing pages for 60 seconds if traffic spikes. Keep the dashboard and anything behind login out of edge cache. Then cache the expensive database query that builds usage summaries inside the app for a short time.

After every step, check what changed. If hit rate goes up and server load drops, keep it. If nothing moves, remove the extra layer and keep the setup simple.

A simple SaaS example

Find the Stale Layer
Oleg can trace where old pages come from and help you simplify the rules.

Picture a small SaaS that sells team scheduling software. It has a public homepage, docs, a pricing page, a dashboard, and an API. The cheapest setup often works best when each cache has one clear job.

Public marketing pages can sit behind Cloudflare with a long cache. The homepage, blog posts, feature pages, and docs usually do not change much during the day, so a cache time of a few hours or even a day is often fine. Cloudflare handles repeat visits near the user, cuts origin traffic, and helps a small server stay calm during a launch or ad spike.

The pricing page needs a different rule. Teams edit plans, trial text, and discount notes more often than they expect. That page can still use Cloudflare, but with a short cache and simple purge rules. Ten minutes is often enough. If the team changes a price and needs it live right away, they purge that single page instead of clearing everything.

For logged-in users, skip edge caching. The dashboard, billing area, and account API should pass through Cloudflare without storing personalized responses there. Inside the app, cache the expensive data instead. Good candidates are plan limits, team permissions, feature flags, and report queries that take hundreds of milliseconds to rebuild. Users still get fast responses, but the app avoids the much bigger risk of showing one customer's data to another.

Nginx fits best where content is public but still costs CPU. A common case is image resizing. Another is a public feed that many visitors request even though it only changes every few minutes. Nginx can keep those responses ready so the app does not repeat the same work.

A clean route map might look like this:

  • Homepage, blog, and docs: Cloudflare long cache
  • Pricing page: Cloudflare short cache
  • Dashboard and user API: no edge cache, app data cache only
  • Image transforms and public feeds: Nginx cache at origin

That is where this setup pays off. If you cache the same full response in Cloudflare, Nginx, and the app, you usually buy yourself one more stale layer to debug. Let Cloudflare absorb public traffic, let Nginx handle repeat origin work, and let the app cache data that is costly to rebuild.

Where duplicate caching wastes time

Duplicate caching looks safe on paper. In practice, it often adds work faster than it adds speed.

A common example is a public page cached by both Cloudflare and Nginx. If the page is small, mostly static, and already cheap to serve, the second cache may save only a few milliseconds. What it definitely adds is a second place that can go stale.

The same problem shows up inside the app. Teams sometimes cache database results in Redis or memory even though Nginx already serves the final response fast and the page barely changes. That extra app cache adds code paths, invalidation rules, and one more thing to inspect when users report old content.

On a budget, every cache should pay rent. If one layer already handles the job well, adding another layer for the same content is often just busywork.

When overlap becomes a problem

The pain usually starts with TTL drift. Cloudflare might keep a page for 1 hour, Nginx for 10 minutes, and the app cache for 24 hours. Someone updates pricing at noon, clears one cache, and assumes the change is live.

Then support gets three screenshots from three users. One sees the new price, one sees the old page from Cloudflare, and one gets a fresh HTML shell with old data from the app cache. Nobody is sure which layer won because they all expired on different schedules.

That is why duplicate caching wastes time mostly in debugging, not just in setup.

A short check helps:

  • If Cloudflare already caches a public page well, ask whether Nginx needs that same full response.
  • If Nginx serves a response in a few milliseconds, ask whether the app needs its own cache for the same output.
  • If you cannot explain purge order in one sentence, you probably have too many caches.

Updates make this worse. One change can mean purging Cloudflare, clearing Nginx cache files, deleting Redis entries, and sometimes warming pages again. The work multiplies, but the user only cares whether the page is correct.

A simpler setup is usually better: let Cloudflare handle public edge caching, let Nginx handle reverse proxying and maybe a few expensive responses, and let the app cache only data that is truly slow to compute.

Mistakes that create stale or wrong content

Place Caches with Purpose
Review which routes belong at the edge, at the origin, or inside the app.

The fastest way to break a site is to cache something that changes from one user to another. A dashboard, cart, account page, or pricing page with user-specific discounts should never sit in a public cache just because it looked fine in one test.

Cookies and auth headers are the usual trap. If a response depends on a login cookie, session token, or Authorization header, treat it as private unless you have a very clear rule that strips personal data first. One bad rule at the Cloudflare or Nginx layer can show Alice the page meant for Bob.

Cache keys cause a quieter kind of damage. A page may look correct for most people and still be wrong for users on mobile, in another language, or with a filter in the URL. If your app serves different output by locale, device, or query parameters, the cache key must include those differences.

Common misses look like this:

  • English and French pages share one cache entry.
  • Mobile and desktop layouts share one cached response.
  • ?page=2 or ?sort=price gets ignored.
  • A trial user and a paid user hit the same cached HTML.

Long TTLs make all of this worse. Teams often set a one-hour or one-day cache first and promise to fix invalidation later. That order is backward. Test how you purge or refresh content before you increase the TTL. Start with a short lifetime, confirm that updates appear when they should, and only then stretch it where the rules hold up.

Another mistake is blaming every slow route on missing cache. Some routes are slow because the query is bad, the app does too much work, or the page calls several services before it renders. Caching can hide that for a while, but it will not fix broken logic. If invalidation is hard or the content changes every few seconds, the route itself probably needs work.

This matters even more when you are trying to keep infrastructure lean. Every extra layer makes debugging harder. When content looks stale, you now have to ask which layer kept it, which rule matched, and whether the app already sent the wrong headers. Public caching should be reserved for content that is truly shared, predictable, and easy to refresh.

Quick checks before adding another layer

Speed Up Hot Routes
Target the slow queries and public responses that actually cost your team money.

Another cache layer only pays for itself when it removes real work. If it does not cut CPU time, database load, or origin traffic, it mostly adds more rules to remember when something breaks.

In many setups, the cheapest win sits at the edge. A public image, font, or JS bundle should usually get cached by Cloudflare before your server touches it. If Cloudflare already does that job well, adding the same cache again in Nginx mostly gives you more purge steps and more chances to serve stale files.

Run through these checks before you add anything:

  • Find the routes that burn the most CPU or trigger the most database queries.
  • Check whether Cloudflare can cache the response safely.
  • Look at what Nginx already does for you, such as headers, TLS termination, rate limits, and short microcaching for bursts.
  • Add app caching only when it skips real work inside the app, such as repeated queries, slow template rendering, or a costly third-party API call.
  • Make sure your team can purge, trace, and explain cache behavior quickly.

A simple test helps. Pick one hot route, measure it without cache, then with Cloudflare, then with Nginx, then with an app cache. Compare origin requests, response time, and database queries. If one layer already removes most of the load, stop there.

Nginx still earns its place when it shields the app from spikes or cleans up bad request patterns before they reach your code. App caching earns its place when it cuts repeated work that Nginx and Cloudflare cannot see, like a heavy SQL query behind a logged-in dashboard.

If your team cannot purge one user-facing page, confirm the new response, and explain why it changed, do not add another cache.

What to do next

Run two small tests before you touch the whole stack. Pick one public route, like a pricing page or docs page, and one expensive query inside the app, like a dashboard total that hits the database on every request. Those two checks usually show where the biggest gain is.

Then change one layer at a time. If you enable Cloudflare caching, Nginx response caching, and app caching all at once, you lose the trail. You may see fewer origin requests, but you will not know which rule helped and which one only added cleanup work.

Simple usually wins. A small team can learn a lot from one route and one query if they measure before and after:

  • Measure the route first: response time, origin CPU, database calls, and request count.
  • Add one cache layer and let it run long enough to show a real hit rate.
  • Keep the rule only if it lowers origin work in a clear way.
  • Remove rules that add purge work, stale content, or duplicate storage without a solid drop in load.

Be strict about proof. A cache that saves a few milliseconds but creates invalidation bugs is usually a bad trade. A cache that cuts half of repeated database reads is worth keeping.

For many budget setups, the lean version is enough: Cloudflare handles static files and public edge traffic, Nginx handles request buffering and short response caching where it fits, and the app caches expensive query results with clear expiration rules. Anything beyond that should earn its place.

If your stack already feels messy, a quick outside review can help. Oleg Sotnikov at oleg.is works as a Fractional CTO and startup advisor, and this kind of cache cleanup fits that role well. Sometimes the best performance win is removing a layer you never needed.

Frequently Asked Questions

Should I cache the same page in Cloudflare and Nginx?

Usually, no. Let one layer own each job. Cloudflare should cache shared static files and some public pages, Nginx should cache a small set of expensive public responses if your origin still runs hot, and the app should cache data that only your code understands.

When you cache the same full response in two or three places, you get more purge steps and more stale states without much extra speed.

What should Cloudflare cache first?

Start with versioned static assets like CSS, JavaScript, fonts, and images. Those files change rarely, and every edge hit saves bandwidth and origin work.

After that, you can test a few public pages that stay stable for a while, such as docs or blog posts. Keep user pages, login flows, and private API responses out of edge cache.

When does Nginx cache actually help?

Nginx helps when your app keeps rendering the same public response over and over. Short caching for public HTML, image transforms, or a public feed can cut CPU use at the origin.

If Cloudflare already removes most of the traffic, Nginx may not need to cache much. It still earns its place for headers, compression, routing, and request handling.

What belongs in the app cache?

Put slow, repeated work there. Good examples include heavy SQL queries, computed dashboard totals, permission checks, feature flags, and reports that many users request again and again.

The app knows which data changes by user, account, plan, or tenant. That makes it the safest place to cache private or context-aware data.

Should logged-in pages ever sit in edge cache?

Keep them out of public edge cache by default. If a page depends on cookies, session state, account data, or an Authorization header, treat it as private.

You can still cache the expensive parts inside the app. That cuts database work without risking one user's data showing up for someone else.

How long should I set cache TTLs?

Use short TTLs first. A public page might start at 30 to 120 seconds in Nginx, while static assets can live much longer at the edge if you version file names.

Match the TTL to how often the content really changes. Do not jump to one hour or one day until you prove your purge flow works.

Why do I still see old content after a purge?

Most of the time, one layer still holds the old version. Cloudflare, Nginx, and the app can all keep different copies with different TTLs.

Check which layer answered the request, then clear only that layer and confirm the response headers. If you cannot explain the purge order in one sentence, your setup probably has too much overlap.

How do I know another cache layer is worth it?

Measure one hot route before and after you add it. Watch origin requests, response time, CPU load, and database queries.

Keep the layer only if it removes real work. If it saves a few milliseconds but adds stale content and more cleanup, drop it.

What usually makes cached pages show the wrong content?

Cache keys break correctness more often than people expect. If your output changes by language, device, query string, plan, or tenant, your cache key must include that difference.

A page can look fine in one browser and still be wrong for mobile users, French users, or anyone on ?page=2 or ?sort=price.

What is a safe starter setup for a small SaaS?

For a small SaaS, keep it lean. Let Cloudflare serve static assets and stable public traffic, let Nginx handle proxy duties and maybe a short cache for a few expensive public responses, and let the app cache slow query results.

Change one layer at a time and measure the result. That setup stays fast and gives your team fewer places to debug when content goes stale.