Web App Caching Strategy: CDN, Redis and Browser Layers Explained
Most slow web apps are not slow because the code is bad. They are slow because the same expensive work is done again and again. The same product list is queried from the database a thousand times an hour. The same image is fetched from a distant server by every visitor. The same report is recalculated each time someone opens a dashboard.
Caching solves this by remembering results so the work happens once. Done well, it makes pages faster, lowers infrastructure cost and helps your app survive traffic spikes. Done badly, it shows users stale prices, leaks private data or hides bugs. This guide explains a layered caching strategy for web apps, with practical rules for what to cache, where, and for how long.
The four layers of caching
A request can be answered at several points between the user and your database. The closer to the user, the faster and cheaper the response.
1. Browser cache
The user's own browser stores files such as scripts, stylesheets and images. A returning visitor loads them from disk with no network request at all. You control this with HTTP headers like Cache-Control and ETag.
2. CDN cache
A content delivery network keeps copies of your files on servers around the world. A visitor in Mumbai gets assets from a nearby location rather than from a single origin server in another continent. CDNs can also cache full HTML pages and some API responses.
3. Application cache
Inside your infrastructure, an in-memory store such as Redis holds computed results: query outputs, session data, rendered fragments and rate limit counters. This layer protects your database from repeated work.
4. Database cache
Databases keep frequently used data pages in memory and may cache query plans. You mostly benefit from this by good indexing and sufficient memory rather than by managing it directly.
A real-world example: a catalogue-heavy online store
Here is an illustrative scenario. An online store has a few thousand products, and its category pages run several database queries each, including filters, prices and stock counts. During a festival sale, traffic surges and the database struggles. Pages slow down exactly when customers are most eager to buy.
The team adds layers. Static assets get long-lived browser caching with fingerprinted filenames. Images and category pages go through a CDN with a short expiry. Product lists and filter counts are cached in Redis for a minute or two. Stock counts and cart contents remain uncached or very short-lived, since they must be accurate. The database now sees only a fraction of the earlier load, and the pages feel faster. A change in price triggers an explicit purge of the affected product and category pages so customers never see outdated amounts.
The lesson is that not everything deserves the same treatment. Static and semi-static data are cached aggressively; transactional data is cached carefully or not at all.
Step-by-step: designing a caching strategy
- Measure first. Use profiling and tracing to find the slowest routes and the most repeated queries. Cache what the data shows, not what you guess.
- Classify your data. Sort content into static (never changes), semi-static (changes occasionally), personalised (differs per user) and real-time (must be exact).
- Set browser rules for assets. Fingerprint filenames (for example app.3f9a1c.js) and use long expiry such as a year with the immutable directive, so a new deploy simply uses a new filename.
- Put a CDN in front. Cache images, fonts and downloads. For HTML and public API responses, set explicit s-maxage values and use stale-while-revalidate so users get quick responses while the cache refreshes in the background.
- Add Redis for expensive reads. Cache the output of slow queries and computations, keyed carefully (for example products:category:42:page:1), with a time to live that matches how fresh the data must be.
- Choose a pattern. Cache-aside (check the cache, fall back to the database, then store) is the most common and easiest to reason about. Write-through and write-behind exist, but add complexity.
- Define invalidation per data type. Decide whether each entry expires on a timer, is purged on change, or both. Document who owns each rule.
- Prevent cache stampedes. When a popular key expires, many requests can hit the database at once. Use request coalescing, locks, jittered expiry or serve stale data while one worker refreshes.
- Protect private data. Mark personalised responses private or no-store, and use Vary headers correctly so a CDN never serves one user's page to another.
- Monitor and tune. Track hit ratio, eviction rate, memory use and latency, and review them after each major release. Tracing tools help here, as covered in our guide to performance budgets for web apps.
Cache invalidation without the headaches
The famous joke says there are only two hard problems in computing, and cache invalidation is one of them. These approaches make it manageable:
- Time-based expiry: simplest and safest. Accept that data may be slightly stale for the duration of the time to live.
- Event-based purge: when a record changes, delete or update the related keys. This keeps data fresh but requires you to know every place it is cached.
- Versioned keys: include a version number in the key, and bump it when data changes. Old entries simply expire unused.
- Tag-based invalidation: attach tags such as product:42 to cached items, then purge by tag on update. Many CDNs support this.
- Stale-while-revalidate: serve the old value instantly and refresh quietly, good for dashboards and listings where a few seconds of staleness is fine.
Whichever you choose, write down the maximum acceptable staleness for each data type. That single sentence prevents most arguments later.
Common mistakes
- Caching errors: storing a failed response and serving it for minutes. Cache only successful results, or use very short expiry for failures.
- Unbounded keys: building keys from free-form user input, which fills memory with useless entries.
- Using the cache as the only copy: caches can be flushed or evicted at any time; the primary database must remain the source of truth.
- Hiding slow queries: a cache masks a bad query until the first miss or the next deploy. Fix the root cause too, using the techniques in our guide to indexing and tuning slow queries.
- Ignoring cold starts: after a flush or deploy, an empty cache can overload the database. Warm critical keys or ramp traffic gradually.
- Leaking private responses: missing Vary or Cache-Control: private on personalised pages.
Redis tips for production
- Set a maximum memory limit and an eviction policy such as allkeys-lru so Redis never exhausts the host.
- Always set an expiry on cache keys. Keys without one tend to accumulate forever.
- Store compact values and compress large ones. Memory is the cost driver.
- Namespace keys by environment and application to avoid collisions.
- Plan for Redis being unavailable: the app should fall back to the database slowly rather than crash.
- Secure it with private networking and authentication, never exposing it to the public internet.
Key benefits of a layered strategy
- Faster pages: fewer network hops and fewer database calls improve loading metrics that users and search engines notice.
- Lower cost: less compute and database capacity is needed to serve the same traffic.
- Resilience: cached content can keep serving visitors during traffic spikes or brief backend problems.
- Global reach: CDNs bring your app closer to users in different regions.
- Room to grow: you can postpone expensive re-architecture because load grows more slowly than traffic.
HTTP cache headers cheat sheet
Many caching bugs come from misunderstood headers. These are the ones you will use most often:
- Cache-Control: max-age=31536000, immutable for fingerprinted static files that never change at the same URL.
- Cache-Control: public, s-maxage=60, stale-while-revalidate=300 for public pages that can be a little stale on a shared cache.
- Cache-Control: private, no-store for pages containing personal or sensitive information.
- ETag and Last-Modified to let browsers ask "has this changed?" and receive a lightweight 304 response.
- Vary: Accept-Encoding (and others where needed) so caches store separate variants correctly.
Check your configuration with browser developer tools and a command-line request, rather than trusting framework defaults. Many frameworks add conservative headers that quietly disable caching on pages that could safely be cached.
Caching in modern frameworks
Frameworks such as Next.js, Nuxt and Astro offer built-in options like static generation, incremental regeneration and edge caching. They are powerful, but they can hide complexity. Know which pages are generated at build time, which are regenerated on a timer, and which are rendered per request. Document these decisions, because a developer who changes a data fetch can unintentionally turn a cached page into a dynamic one, or the reverse, and the effect only appears under load.
Measuring the impact
Before changing anything, record median and slow-tail response times, database queries per request and origin server load. After each change, repeat the measurements under similar conditions. For example, you might discover that browser and CDN caching improve the loading of static assets considerably, while Redis mainly reduces database pressure rather than visible page speed. This evidence helps you decide where the next hour of effort should go, and it gives non-technical stakeholders a clear picture of why caching work is worth the time.
When not to cache
Skip caching for data that must be exact at every moment, such as account balances, payment status, inventory at the final checkout step and security decisions. Also skip it when the underlying query is simply slow because of a missing index, since fixing the cause is better than hiding it. Caching is a tool for repeated, read-heavy work, not a cure for every performance complaint.
Conclusion
Caching is one of the highest-return performance improvements available, but it rewards discipline. Measure first, classify your data, cache aggressively where content is static, cache carefully where it is personal or transactional, and write down your invalidation rules. Start with browser and CDN caching, add Redis for the expensive reads, and monitor hit rates as you go. If you are planning a high-traffic product or struggling with slow pages, our web development team can audit your architecture and design a caching layer that fits your traffic and your budget.
Frequently Asked Questions
- What should I cache first?
- Start with static assets on a CDN, then cache expensive, read-heavy data such as product listings or computed reports in Redis. Measure before and after so you cache what actually matters.
- Why is cache invalidation considered hard?
- Because stale data can mislead users, and knowing exactly when to remove or refresh cached values across several layers requires clear rules about ownership and expiry.
- Is Redis a database or a cache?
- Redis is an in-memory data store commonly used as a cache. It can persist data, but most teams treat it as disposable and keep the source of truth in a primary database.
- Can caching cause security problems?
- Yes. Caching personalised or private responses on a shared CDN can leak one user's data to another. Use correct cache headers and vary rules, and never cache authenticated responses publicly.
- How do I know my cache is working?
- Track hit ratio, latency and origin load. A healthy cache shows a high hit rate on cacheable routes and a visible drop in database queries.