Caching is often the single highest-impact performance improvement available — avoiding repeated expensive work (database queries, API calls, rendering) by storing and reusing results. This guide covers the main caching layers and when to use each.
The Caching Hierarchy
| Layer | What It Caches | Speed Benefit |
|---|---|---|
| Browser cache | Static assets on the visitor's device | Eliminates repeat requests entirely |
| CDN cache | Static content at edge locations | Reduces origin server load, faster for distant visitors |
| Web server cache (Nginx) | Full page or proxy responses | Avoids hitting the application at all |
| Application/object cache (Redis) | Database query results, computed values | Avoids expensive database/computation work |
| Database query cache | Query execution plans, some result caching | Reduces database CPU load |
Layer 1 — Browser Caching Headers
For Nginx, add caching headers to static assets:
location ~* \.(jpg|jpeg|png|gif|css|js|woff2)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
Layer 2 — CDN Caching
See How to Set Up a CDN in Front of Your VPS for offloading static content delivery entirely.
Layer 3 — Nginx Proxy Caching
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m max_size=1g inactive=60m;
server {
location / {
proxy_cache app_cache;
proxy_cache_valid 200 10m;
proxy_pass http://127.0.0.1:3000;
}
}
Be careful: never cache personalized or logged-in-user content this way without explicit cache-bypass rules for those requests.
Layer 4 — Application-Level Caching with Redis
Cache expensive database query results:
// Node.js example
const cached = await redis.get('top-products');
if (cached) {
return JSON.parse(cached);
}
const products = await db.query('SELECT * FROM products ORDER BY sales DESC LIMIT 10');
await redis.set('top-products', JSON.stringify(products), 'EX', 300);
return products;
See How to Install and Secure Redis on Ubuntu & Debian for setup.
Choosing What to Cache
- Good candidates: data that's expensive to compute/query and doesn't change on every request (product listings, aggregated statistics, rendered page fragments)
- Poor candidates: data that's already fast to fetch, or changes on every single request (real-time inventory counts during checkout, user-specific session data)
Cache Invalidation Strategies
| Strategy | How It Works |
|---|---|
| Time-based expiry (TTL) | Simplest; cache expires automatically after N seconds |
| Explicit invalidation | Application code deletes the cache entry whenever the underlying data changes |
| Write-through | Cache is updated at the same time as the database, staying always in sync |
Common Caching Bugs
- Stale data shown after an update — the cache wasn't invalidated when the underlying data changed
- Cache showing one user's data to another — personalized content was cached without including the user identity in the cache key
- Cache stampede — many simultaneous requests all miss the cache at once (e.g. right after expiry) and hit the database simultaneously; mitigate with staggered expiry times or a locking mechanism
Measuring the Impact
Compare response times before and after adding caching using the same curl timing approach from How to Profile and Optimize Slow Application Requests.
Related Articles
- How to Install and Secure Redis on Ubuntu & Debian
- Nginx Performance Tuning: Worker Processes, Caching & Gzip
- How to Profile and Optimize Slow Application Requests
