Client Area

CDN vs Application Cache — Where to Cache What

ByDomain India Team·DomainIndia Engineering
6 min readPublished 22 Apr 2026Updated 4 Jun 202699 views

In this article

  • 1The 5 caching layers
  • 2Decision matrix — what goes where
  • 3Layer 1 — Browser cache
  • 4Layer 2 — CDN edge (Cloudflare)
  • 5Cache everything with Page Rule

CDN vs Application Cache — Where to Cache What

TL;DR
Your app has caching opportunities at 5 distinct layers — browser, CDN edge, reverse proxy, app memory, database. Putting the right cache at the right layer cuts response time from 500ms to 20ms. This guide maps every cache decision with DomainIndia + Cloudflare examples.

The 5 caching layers

LayerLatency to userTTL typicalWho sets it
Browser (HTTP cache)0msminutes-daysHTTP headers
CDN edge (Cloudflare)10-50msseconds-daysHTTP headers + rules
Reverse proxy (nginx, Varnish)1-5msseconds-minutesproxy config
Application cache (Redis, memcached)1-2msminutes-hoursapp code
Database (query cache, materialised views)0-10msvariesDB config / app

Each layer catches different things. Use them together.

Decision matrix — what goes where

Content typeBest cache layerTTL
Static assets (CSS, JS, images, fonts)Browser + CDN1 year
Public HTML pages (marketing)CDN1-24 hours
Personalised HTML (dashboard)App cache (per-user)Minutes
DB query results (user profile)App (Redis)Minutes
DB query results (product list)App (Redis) + CDN API cache1-5 min
Third-party API responsesApp (Redis)Hours
Computed views (aggregations)Materialised view + AppHours-days
Session dataApp (Redis)Until expiry
Rate-limit countersApp (Redis)Window

Layer 1 — Browser cache

The cheapest cache. Served without network request at all.

Set HTTP headers:

nginx
location ~* .(jpg|jpeg|png|gif|webp|svg|css|js|woff2|ttf)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    add_header Vary "Accept-Encoding";
}

location /api/ {
    expires -1;   # no cache for API
    add_header Cache-Control "no-store";
}

location / {
    expires 1h;
    add_header Cache-Control "public, must-revalidate";
}

Fingerprinting your static files breaks cache automatically:

bundle.abc123.js — when content changes, hash changes, browser refetches. Set TTL = forever.

Layer 2 — CDN edge (Cloudflare)

Cloudflare caches what you tell it to. By default: static files yes, HTML no.

Cache everything with Page Rule

Cloudflare Dashboard → Caching → Page Rules → Add:

URL: *yourcompany.com/*
Settings:
  Cache Level: Cache Everything
  Edge Cache TTL: 2 hours
  Browser Cache TTL: 30 minutes

Now static HTML pages cache at edge too.

Bypass cache for dynamic paths

URL: *yourcompany.com/admin/*
Settings:
  Cache Level: Bypass

And for cookies:

URL: *yourcompany.com/*
Settings:
  Cache Level: Cache Everything
  Edge Cache TTL: 2h
  Cache by Device Type: On    (separate mobile/desktop variants)
  Bypass Cache on Cookie: (wp_logged_in|sessionid|auth_token)

When auth_token cookie is set → Cloudflare skips cache, hits origin. Logged-out users see cached; logged-in users see fresh.

Cloudflare's own analytics

See hit ratio: Cloudflare → Analytics → Cache.

Target: >70% hit ratio for content sites. Dashboard apps: may be lower but still benefits from static-asset caching.

Purge cache after deploy

bash
curl -X POST "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/purge_cache" 
    -H "Authorization: Bearer YOUR_API_TOKEN" 
    -H "Content-Type: application/json" 
    --data '{"purge_everything":true}'

Or purge specific URLs:

bash
--data '{"files":["https://yourcompany.com/api/users","https://yourcompany.com/"]}'

Add to your deploy pipeline.

Layer 3 — Reverse proxy (nginx / Varnish)

On your VPS — caches after origin generates, before sending to Cloudflare.

nginx:

nginx
proxy_cache_path /var/cache/nginx keys_zone=STATIC:10m max_size=1g inactive=60m;

location /api/ {
    proxy_cache STATIC;
    proxy_cache_valid 200 5m;
    proxy_cache_valid 404 1m;
    proxy_cache_key "$scheme$request_method$host$request_uri";
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    add_header X-Cache-Status $upstream_cache_status;

    proxy_pass http://app;
}

X-Cache-Status: HIT / MISS header shows cache state.

Varnish (alternative, more powerful):

vcl
sub vcl_backend_response {
    set beresp.ttl = 5m;
    set beresp.grace = 1h;    # serve stale during outages
}

See our Server-Side Caching article.

Layer 4 — Application cache (Redis)

The most flexible layer — your code decides what to cache.

See full patterns in our Redis Beyond Caching article.

Classic cache-aside:

typescript
async function getUser(id: string) {
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);
  const user = await db.user.findUnique({ where: { id } });
  await redis.setex(`user:${id}`, 300, JSON.stringify(user));
  return user;
}

Cache tags (Laravel, Rails) — invalidate related keys together:

php
// Laravel
Cache::tags(['users', "user:{$id}"])->put("user.{$id}.profile", $data, 300);
// Later invalidate just this user:
Cache::tags(["user:{$id}"])->flush();

Layer 5 — Database-level

  • Postgres materialized views for expensive aggregations:

```sql

CREATE MATERIALIZED VIEW daily_stats AS

SELECT date(created_at), count(*), sum(amount)

FROM orders GROUP BY 1;

REFRESH MATERIALIZED VIEW daily_stats; -- schedule hourly

```

  • Postgres `pg_prewarm` — load hot tables into shared_buffers on startup
  • MySQL query cache (removed in 8.0; use Redis instead)
  • Read replicas — not caching exactly, but distributes load

Invalidation strategies

Three approaches:

TTL-only (easy, eventual consistency)

Cache: 300s TTL
Updates: ignored
Result: users see stale up to 5 min

Works for: product catalogs, articles, rankings.

Explicit invalidation

typescript
async function updateUser(id, data) {
  await db.user.update({ where: { id }, data });
  await redis.del(`user:${id}`);
}

Works for: user profiles, settings, anything with clear "owner".

Write-through

typescript
async function updateUser(id, data) {
  const user = await db.user.update({ where: { id }, data });
  await redis.setex(`user:${id}`, 300, JSON.stringify(user));
}

Cache always matches DB. Best consistency, most code.

Cache stampede prevention

When cache expires, 1000 concurrent requests all hit DB. Prevent with:

Lock (SETNX):

typescript
async function withLock(key, callback, lockTtl = 10) {
  const lockKey = `lock:${key}`;
  const acquired = await redis.set(lockKey, '1', { NX: true, EX: lockTtl });
  if (acquired) {
    try { return await callback(); } finally { await redis.del(lockKey); }
  }
  // Someone else is computing — wait briefly and try cache again
  await new Promise(r => setTimeout(r, 100));
  return JSON.parse(await redis.get(key));
}

Probabilistic early refresh: (see Laravel Cache article)

Hit ratio targets

Measure. Without data, you're guessing:

LayerTarget hit rate
Browser cache60-80%
Cloudflare>70% (content), >50% (apps)
nginx proxy cache>80% for what you cache
Redis app cache>80%
DB query cache>90% for cached queries

Monitor via:

  • redis-cli INFO statskeyspace_hits / (keyspace_hits + keyspace_misses)
  • Cloudflare Analytics → Cache
  • X-Cache-Status nginx header aggregated in logs

Common pitfalls

FAQ

Q Cloudflare + nginx cache — overkill?

Depends on traffic. Cloudflare handles 80% globally; nginx catches what Cloudflare misses (geographic origin requests). For most sites: Cloudflare alone is fine.

Q How do I cache logged-in users?

Either per-user keys (expensive in Redis memory) or differentiate static vs dynamic parts. See our Next.js App Router article on Partial Prerendering.

Q Does Cloudflare cost extra for high cache usage?

Free plan includes unlimited Cloudflare bandwidth. Origin bandwidth is what's reduced — huge savings.

Q CDN for APIs?

Yes for public read-only APIs. Set short TTL (30-60s) and Cache-Control: public. User-specific APIs: bypass.

Q When does caching hurt?

When it hides bugs (stale data looks like app is broken). When cost of inconsistency > cost of slower response. When debug cycles lengthen ("is it cache or code?").

Combine DomainIndia hosting + Cloudflare for a fast cache stack. View hosting

Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket