Server-Side Caching

CDN vs Application Cache — Where to Cache What

By Domain India Team · DomainIndia EngineeringPublished 9 min read
Knowledge base article
Contents (19 sections)

Caching is the cheapest way to make a site faster, but each cache layer suits different content. This guide maps what to cache where, from the visitor's browser to your database, with examples for Cloudflare, nginx and Redis, and notes on which layers you control on Domain India hosting.

Key takeaways

Your app has caching opportunities at 5 distinct layers: browser, CDN edge, reverse proxy, app memory and database. Putting the right cache at the right layer can cut response times from hundreds of milliseconds to tens. This guide maps every cache decision, with Domain India hosting and 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.

Which layers you control on Domain India

On shared hosting you control the browser layer (headers in .htaccess) and the CDN layer (Cloudflare). Our cPanel servers already run an nginx caching proxy in front of Apache; DirectAdmin servers have no nginx proxy, so Apache answers every request. Redis and Memcached are not available on shared hosting or the App Platform; to run them, or your own nginx or Varnish cache, use a VPS.

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: the browser serves the file without any network request.

Set HTTP headers. On your own nginx server:

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";
}

On Domain India shared hosting (Apache), set the same headers in .htaccess; mod_expires and mod_headers are loaded on our cPanel and DirectAdmin servers:

apache
<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType text/css "access plus 1 year"
  ExpiresByType application/javascript "access plus 1 year"
  ExpiresByType image/webp "access plus 1 year"
</IfModule>

Compression is already handled on our shared servers (gzip through nginx on cPanel, mod_deflate on DirectAdmin), so you don't need to add it yourself.

Fingerprinting your static files breaks the cache automatically:

bundle.abc123.js — when the content changes, the hash changes and the browser fetches the new file. Set TTL = forever.

Layer 2 — CDN edge (Cloudflare)

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

Cache HTML with a Cache Rule

Cloudflare has replaced Page Rules with Cache Rules for caching settings. In the Cloudflare dashboard, open Caching → Cache Rules and create a rule:

code
When: Hostname equals yourcompany.com
Then:
  Cache eligibility: Eligible for cache
  Edge TTL: 2 hours
  Browser TTL: 30 minutes

Now public HTML pages are cached at the edge too.

Bypass cache for dynamic paths and logged-in users

Add a second rule, placed after the first so it wins:

code
When: URI Path starts with /admin/
   OR Cookie contains "wp_logged_in"
   OR Cookie contains "sessionid"
Then:
  Cache eligibility: Bypass cache

When a login cookie is set, Cloudflare skips the cache and goes to your server. Logged-out users get the cached page; logged-in users get fresh pages.

Cloudflare's own analytics

See the hit ratio in Cloudflare → Analytics → Cache.

Target: >70% hit ratio for content sites. Dashboard apps will be lower but still benefit 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 this to your deploy pipeline.

Layer 3 — Reverse proxy (nginx / Varnish)

A reverse proxy caches responses after your app generates them, before they go out to Cloudflare or the visitor.

On Domain India cPanel hosting this layer already exists: nginx runs in front of Apache and can keep 200, 301 and 302 responses for up to 120 minutes (measured 22 September 2026). You can't configure it from your account. If a change doesn't show, add ?t= with a new value to the URL to check. Domain India DirectAdmin hosting has no proxy cache, so use browser headers and Cloudflare there.

On your own VPS, configure nginx yourself:

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;
}

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

Varnish (alternative, more powerful, VPS only):

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. Redis is not available on Domain India shared hosting or the App Platform, so this layer runs on your own VPS; on shared hosting, use your framework's database or file cache instead.

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; cache in your app instead)
  • Read replicas — not caching exactly, but they spread the load

Invalidation strategies

Three approaches:

TTL-only (easy, eventual consistency)

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

Works for: product catalogues, 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 a 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));
}

The cache always matches the database. Best consistency, most code.

Cache stampede prevention

When a popular key expires, 1,000 concurrent requests can all hit the database. Prevent it with:

Lock (SET NX):

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 the cache again
  await new Promise(r => setTimeout(r, 100));
  const cached = await redis.get(key);
  return cached ? JSON.parse(cached) : callback();
}

Stale-while-revalidate / early refresh: see our Laravel + Redis 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%
PostgreSQL buffer cacheAbout 99% for a warm database

Monitor via:

  • redis-cli INFO stats → keyspace_hits / (keyspace_hits + keyspace_misses)
  • Cloudflare Analytics → Cache
  • PostgreSQL: blks_hit / (blks_hit + blks_read) from pg_stat_database
  • X-Cache-Status nginx header aggregated in logs

Common pitfalls

Caching user-specific data publicly
User A sees user B's dashboard. Include the user ID in the cache key, or bypass the cache for logged-in users.
TTL too long
Stale content drives users away. Balance freshness and load; 5 minutes is often right.
No invalidation on write
Changes don't appear until the TTL expires, and the site feels broken.
Caching API responses with personalised fields
Split them: cache the public fields, fetch the user-specific ones fresh.
Cache everything, then inconsistent reads
A user posts a comment, refreshes, and it isn't there. Invalidate on write.
Serialising huge objects in cache
Keep Redis values small, ideally under 1 MB. Cache summaries and fetch the detail.

FAQ

Cloudflare + nginx cache — overkill?

It depends on traffic. Cloudflare answers cached requests at the edge; an nginx cache on the origin catches what reaches your server. For most sites, Cloudflare plus Domain India's cPanel caching proxy is enough.

How do I cache pages for logged-in users?

Either use per-user cache keys (which cost more memory) or separate the static and dynamic parts of the page. See our Next.js App Router article on Partial Prerendering.

Does Cloudflare cost extra for high cache usage?

Cloudflare's plans don't charge for cached bandwidth. The saving is on your origin, which serves fewer requests.

CDN for APIs?

Yes for public, read-only APIs. Set a short TTL (30-60s) and Cache-Control: public. Bypass the cache for user-specific APIs.

Can I use Redis for the application cache on Domain India shared hosting?

No. Redis and Memcached are not available on Domain India shared hosting or the App Platform. Use your framework's database or file cache there, or run Redis on a Domain India VPS.

When does caching hurt?

When it hides bugs (stale data looks like a broken app), when the cost of inconsistency is higher than the cost of a slower response, and when debugging slows down because you can't tell whether the cache or the code is at fault.

Ready to build a fast cache stack? Put Cloudflare in front of cPanel hosting, or choose a VPS to run Redis and your own proxy cache.

Fast hosting with a caching proxy built in

cPanel hosting runs an nginx caching proxy in front of Apache. Add Cloudflare for edge caching.

View cPanel hosting

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app