Web Application Caching Strategies: Redis, CDN & HTTP
A web application can feel fast with 20 test records and become painfully slow after thousands of records, repeated API requests, large images, and concurrent users are added.
The usual reaction is to scale the server. Often, the better first question is simpler:
Why is the application repeating work it has already completed?
Caching stores reusable data closer to where it is needed. It can prevent repeated downloads, database queries, API calls, template rendering, and expensive calculations.
However, a cache is temporary acceleration—not the source of truth. A fast response is useless when it contains another user’s data, an outdated permission, or yesterday’s stock quantity.
This guide explains where caching belongs, how to choose a caching strategy, and how to implement cache-aside without making Redis a single point of failure.
Quick Answer: Which Caching Strategy Should You Use?
For most read-heavy web applications, start with a layered approach:
- Cache fingerprinted CSS, JavaScript, fonts, and images in the browser.
- Use a CDN for public static files and identical public responses.
- Use cache-aside with Redis for repeated database reads shared across application instances.
- Give every application-cache entry a time-to-live.
- Invalidate affected keys after a successful database update.
- Add TTL jitter and request coalescing for popular keys.
- Keep sensitive, authorization-dependent, and immediately consistent values out of shared caches.
- Measure hit ratio, p95 latency, database-query volume, memory, evictions, and stale-data incidents.
Cache-aside is usually the safest starting pattern because the database remains authoritative and only requested data is cached. Redis and AWS both identify cache-aside or lazy caching as a common foundation for read-heavy workloads.
What Caching Is—and What It Is Not
A cache stores a temporary copy of data or a computed result.
A cache hit serves that copy. A cache miss retrieves the value from the source and may populate the cache for later requests.
Caching is not persistence. The application must still work when the cache is empty, restarted, unavailable, or stale.
Main Caching Layers
|
Layer |
Best for |
Main advantage |
Main risk |
|
Browser cache |
Versioned CSS, JS, fonts, images |
Avoids repeated downloads |
Old assets without versioned URLs |
|
Service worker |
App shell and offline resources |
Controlled offline behaviour |
Complex update lifecycle |
|
CDN or edge |
Public assets, pages, and APIs |
Lower latency and origin load |
Incorrect shared-cache keys |
|
Reverse proxy |
Rendered pages and public APIs |
Protects application servers |
Private-data leakage |
|
In-process L1 |
Configuration and small hot objects |
No network call |
Different values on each instance |
|
Redis or Memcached L2 |
Shared queries, sessions, aggregates |
Shared across instances |
Memory and invalidation complexity |
|
Database cache |
Buffer pages and execution data |
Automatic acceleration |
Limited application control |
The layers should complement one another rather than duplicate the same policy. AWS similarly describes caching as something that can be applied across browser, network, application, and database layers.
How to Choose a Caching Strategy
Ask five questions before writing code:
- How frequently is the data read compared with how often it changes?
- How stale may the value become without harming the user?
- Is the response public, tenant-specific, role-specific, or user-specific?
- What happens when the cache fails?
- Can the application identify every key affected by a write?
|
Workload |
Consistency need |
Recommended starting pattern |
|
Read-heavy, infrequent writes |
Moderate |
Cache-aside |
|
Frequently read after every write |
High |
Write-through with cache-aside fallback |
|
Very high write throughput |
Eventual |
Write-behind with durability controls |
|
Predictable hot data |
Moderate |
Refresh-ahead or cache warming |
|
Public content |
Slight staleness acceptable |
CDN with stale-while-revalidate |
|
Small per-instance configuration |
Low change frequency |
In-process cache |
|
Shared multi-instance data |
Moderate |
Redis or another distributed cache |
Avoid caching payment status, permissions, account balances, one-time tokens, or live inventory unless the consistency model and failure consequences are explicitly designed.
AWS recommends evaluating whether cached data is safe, whether the access pattern benefits from caching, and whether the value has been structured appropriately for cache retrieval.
Core Caching Patterns
Cache-Aside
The application checks the cache first. On a miss, it reads from the database, stores the result with a TTL, and returns it.
This pattern works well for project pages, categories, public profiles, configuration, and dashboard summaries.
Read-Through and Write-Through
Read-through moves cache-miss loading behind a cache abstraction.
Write-through updates the cache whenever the primary store changes. It can improve post-write reads but adds write latency and may fill memory with records that are rarely requested.
Write-Behind
Write-behind persists changes to the database later. It can increase throughput but introduces durability, ordering, and recovery risks.
Refresh-Ahead and Stale-While-Revalidate
Refresh-ahead updates hot values before expiry. Stale-while-revalidate temporarily serves an older response while refreshing it.
Practical HTTP Cache-Control Recipes
Use policies that match the resource rather than applying one global header.
Fingerprint assets
Cache-Control: public, max-age=31536000, immutable
Personalized dashboard that must revalidate
Cache-Control: private, no-cache
ETag: "dashboard-version"
Highly sensitive response
Cache-Control: no-store
Public API cached longer at the CDN
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=30, stale-if-error=86400
Vary: Accept-Encoding, Accept-Language
no-cache permits storage but requires revalidation. no-store tells caches not to store the response. MDN generally recommends private, no-cache for personalized content when storage with revalidation is acceptable.
Do not put cookies or broad User-Agent variation into cache keys without understanding the effect. Excessive variants reduce cache reuse, while omitting a user, tenant, role, language, or query dimension can expose the wrong response.
Multi-Level Caching: L1, L2, and Source
A scalable application may use:
Request → L1 in-process cache → L2 Redis cache → database or external API
L1 avoids a network round trip, but each application instance owns a separate copy. L2 provides shared data and more centralized invalidation.
Use this architecture only when the additional hit rate justifies its cache-coherence complexity. Redis notes that local caches warm independently across stateless instances and are harder to invalidate consistently.
A Resilient Cache-Aside Example
const NOT_FOUND = "__NOT_FOUND__";
async function getProject({ tenantId, projectId }) {
const key = `project:v2:tenant=${tenantId}:id=${projectId}`;
try {
const cached = await redis.get(key);
if (cached !== null) {
metrics.increment("project_cache_hit");
if (cached === NOT_FOUND) return null;
try {
return JSON.parse(cached);
} catch (error) {
logger.warn({ key, error }, "Invalid cached JSON");
await redis.del(key);
}
}
metrics.increment("project_cache_miss");
} catch (error) {
metrics.increment("project_cache_error");
logger.warn({ key, error }, "Redis unavailable; using database");
}
const project = await db.projects.findByTenantAndId(
tenantId,
projectId
);
try {
const ttl = project
? 300 + Math.floor(Math.random() * 30)
: 30;
await redis.set(
key,
project ? JSON.stringify(project) : NOT_FOUND,
{ EX: ttl }
);
} catch (error) {
logger.warn({ key, error }, "Cache refill failed");
}
return project;
}
This implementation supports negative caching, tenant isolation, TTL jitter, database fallback, and cache-error logging.
Add short Redis timeouts and bounded retries in the client configuration. An optional read cache should normally cause slower responses—not complete application failure—when it is unavailable.
Cache Invalidation and Race Conditions
A common update flow is:
- Update the database.
- Delete the detail key.
- Delete or version related list and aggregate keys.
- Allow the next read to repopulate the cache.
Concurrency can still reintroduce stale data:
- Request A misses and reads the old database value.
- Request B updates the database and deletes the key.
- Request A writes the old value back into the cache.
Possible mitigations include:
- Versioned keys
- Record-version checks
- Delayed double deletion
- Transactional outbox
- Event-driven invalidation
- Change data capture
- Compare-before-set logic
- Short TTL combined with event invalidation
The correct approach depends on the business cost of serving stale data.
Prevent Cache Penetration, Stampedes, and Avalanches
Cache penetration occurs when repeated requests for nonexistent records keep reaching the database. Use short-lived negative caching, validation, and rate limiting.
A cache stampede occurs when many requests miss the same popular key simultaneously. Use request coalescing, a short lock with a timeout, refresh-ahead, stale-while-revalidate, or a single-flight library.
A cache avalanche occurs when many keys expire together. Add TTL jitter, stagger refreshes, and avoid identical expiration times for large key groups.
Monitor hot keys separately. One extremely popular key can overload a cache node even when the overall hit ratio appears healthy.
Memory and Eviction Planning
Estimate memory using:
required memory ≈ key count × average serialized object size
+ key overhead
+ allocator and replication overhead
Track cardinality, object size, memory fragmentation, and eviction rate.
LRU favours recently used entries. LFU favours frequently used entries. Redis and AWS document several configurable eviction policies that behave differently under memory pressure.
Implementation and Testing Checklist
- Measure baseline p50, p95, and p99 latency, query count, database time, CPU, and error rate.
- Select one safe, read-heavy endpoint.
- Define the complete key, TTL, acceptable staleness, and invalidation triggers.
- Implement cache-aside with database fallback.
- Use negative caching only for genuine “not found” results.
- Protect popular keys from stampedes.
- Test Redis downtime, malformed values, memory pressure, and concurrent updates.
- Repeat the same load test and publish real measurements.
A credible benchmark states the dataset, software versions, test machine, virtual-user count, duration, hit ratio, latency percentiles, database-query reduction, memory use, and failure behaviour.
Never invent performance percentages.
Frequently Asked Questions
Is Redis required for web application caching?
No. Browser caching, CDN caching, service workers, in-process memory, reverse proxies, and database caches can all be useful.
Redis becomes valuable when multiple application instances need a shared low-latency cache with TTL and eviction controls.
Redis vs Memcached: which should I choose?
Choose Memcached for a simple distributed key-value cache.
Choose Redis when you require richer data structures, atomic operations, persistence options, streams, or distributed-lock patterns. Benchmark both using your actual workload.
What is the best cache TTL?
There is no universal TTL. Base it on update frequency, acceptable staleness, miss cost, traffic, and risk.
Use longer TTLs for versioned immutable assets and shorter TTLs for rapidly changing summaries.
What is the difference between no-cache and no-store?
no-cache allows storage but requires revalidation before reuse.
no-store instructs caches not to store the response.
Should API responses be cached?
Public idempotent responses can be good candidates. Personalized responses require private policies and cache keys containing every relevant variation dimension.
How can students demonstrate caching in a viva?
Show the architecture, key format, TTL, cache miss, cache hit, database update, invalidation, refreshed response, Redis-down fallback, and before-and-after measurements.
Can caching create security vulnerabilities?
Yes. Incorrect shared-cache rules can expose personalized data or contribute to cache poisoning and web cache deception.
Separate public and private policies, normalize keys, validate forwarded headers, and never assume that authentication automatically prevents caching. Research into web cache deception has demonstrated that misconfigured caching infrastructure can expose private information and authentication data.
Conclusion
Effective web application caching strategies reduce repeated work without weakening correctness, security, or resilience.
Start with measurement, cache one safe read-heavy endpoint, and use cache-aside with a documented TTL and invalidation rule. Apply browser and CDN policies to public resources, use Redis for shared application data, and add multi-level caching only when the measured benefit justifies the complexity.
For a practical implementation, explore Node.js projects with source code and working project demos. Document the cache-hit, cache-miss, invalidation, and fallback flow as part of the project architecture.
The article is approximately 1,700 words, excluding schema and supporting audit material.