AWS Builder Center

Caching Strategies: Make Your App 10x Faster Without Breaking the Bank

Your app is slow. Caching fixes it. Learn 5 caching types: client-side (localStorage), server memory, Redis, HTTP headers, and CDNs. Master patterns like cache-aside and write-through. Understand TTL, handle stale data, prevent cache stampede. Real-world examples from e-commerce. See 20-50x performance improvements with proper caching. Includes checklist for what to cache and when. Transform slow queries into instant responses.

Caching Strategies: Make Your App Fast Without Breaking the Bank

Your app is slow. Users are leaving. You add a database index. Still slow. Then someone mentions caching and suddenly everything is 10x faster.
Caching is one of the most underrated performance hacks in development. It's simple, it works, and it can save you thousands in infrastructure costs.
Let me show you how to actually use it.

🎯 What Even Is Caching?

Caching is storing data somewhere fast so you don't have to compute or fetch it again.
Without cache:
  1. User requests data
  2. Query database (slow)
  3. Process data (slow)
  4. Return to user
  5. Wait 500ms
With cache:
  1. User requests data
  2. Check cache (instant)
  3. Found it! Return to user
  4. Wait 5ms
That's 100x faster. And that's just the beginning.

🏗️ Types of Caching

1. Client-Side Caching (Browser)

Store data in the user's browser.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Cache API response in localStorage
async function fetchUser(id) {
const cached = localStorage.getItem(`user-${id}`);
if (cached) {
return JSON.parse(cached);
}

const response = await fetch(`/api/users/${id}`);
const data = await response.json();

// Store for 1 hour
localStorage.setItem(`user-${id}`, JSON.stringify(data));

return data;
}
Pros:
  • Zero server load
  • Instant for returning users
  • Works offline
Cons:
  • Limited storage (~5MB)
  • Data can get stale
  • User can clear it anytime
Use when: Data doesn't change often (user profile, settings)

2. Server-Side Caching (In-Memory)

Store data in your application's memory.
1
2
3
4
5
6
7
8
9
10
11
12
const cache = {};

function getUser(id) {
if (cache[`user-${id}`]) {
return cache[`user-${id}`];
}

const user = db.query(`SELECT * FROM users WHERE id = ${id}`);
cache[`user-${id}`] = user;

return user;
}
Pros:
  • Very fast
  • Reduces database load
  • Simple to implement
Cons:
  • Lost when app restarts
  • Only works on single server
  • Memory usage grows
Use when: You have a small amount of frequently accessed data

3. Redis: The Professional Cache

Redis is an in-memory data store built for caching.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const redis = require('redis');
const client = redis.createClient();

async function getUser(id) {
// Check cache first
const cached = await client.get(`user-${id}`);
if (cached) {
return JSON.parse(cached);
}

// Cache miss - query database
const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);

// Store in cache for 1 hour (3600 seconds)
await client.setex(`user-${id}`, 3600, JSON.stringify(user));

return user;
}
Pros:
  • Ultra-fast
  • Distributed (works across multiple servers)
  • Supports expiration
  • Can store any data type
Cons:
  • Another service to manage
  • Need to handle cache misses
  • Data loss if Redis crashes (but recoverable)
Use when: High traffic, need consistency across servers

4. HTTP Caching (Browser + CDN)

Use HTTP headers to tell browsers and CDNs to cache responses.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.get('/api/products', (req, res) => {
// Cache for 1 hour in browser
res.set('Cache-Control', 'public, max-age=3600');

const products = db.query('SELECT * FROM products');
res.json(products);
});

app.get('/api/user', (req, res) => {
// Don't cache (personal data)
res.set('Cache-Control', 'private, no-cache');

const user = db.query('SELECT * FROM users WHERE id = ?', [req.user.id]);
res.json(user);
});
Pros:
  • Free (browsers handle it)
  • Works with CDNs
  • No code changes needed
  • Reduces server load
Cons:
  • Browser controls it (can't force invalidation)
  • Public data only (be careful with secrets)
Use when: Public, static-ish data (products, articles)

5. CDN Caching (CloudFront, Cloudflare)

Store copies of your content on servers worldwide.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// AWS CloudFront setup
const cloudfront = new AWS.CloudFront();

await cloudfront.createDistribution({
DistributionConfig: {
Origins: [{
DomainName: 'myapp.com',
Id: 'myOrigin'
}],
DefaultCacheBehavior: {
ViewerProtocolPolicy: 'redirect-to-https',
CachePolicyId: '658327ea-f89d-4fab-a63d-7e88639e58f6', // Managed policy
TargetOriginId: 'myOrigin'
}
}
}).promise();
Pros:
  • Global reach
  • Super fast for distributed users
  • Handles spikes
  • DDoS protection included
Cons:
  • Costs money
  • Cache invalidation tricky
  • Overkill for small apps
Use when: High traffic, global users, or static assets

💡 Caching Patterns

Pattern 1: Cache-Aside (Lazy Loading)

1
2
3
4
1. Check cache
2. If miss, query DB
3. Store in cache
4. Return to user
Code:
1
2
3
4
5
6
7
8
9
async function getPost(id) {
const cached = await redis.get(`post-${id}`);
if (cached) return JSON.parse(cached);

const post = await db.query('SELECT * FROM posts WHERE id = ?', [id]);
await redis.setex(`post-${id}`, 3600, JSON.stringify(post));

return post;
}
Best for: Read-heavy applications

Pattern 2: Write-Through

1
2
3
1. Write to cache AND database simultaneously
2. If both succeed, return success
3. If either fails, rollback
Code:
1
2
3
4
5
async function updatePost(id, data) {
const updated = await db.query('UPDATE posts SET ? WHERE id = ?', [data, id]);
await redis.setex(`post-${id}`, 3600, JSON.stringify(updated));
return updated;
}
Best for: When data consistency is critical

Pattern 3: Cache Invalidation on Change

1
2
3
1. Update database
2. Delete cache entry
3. Next read rebuilds cache
Code:
1
2
3
4
async function deletePost(id) {
await db.query('DELETE FROM posts WHERE id = ?', [id]);
await redis.del(`post-${id}`);
}
Best for: When you need fresh data

🎯 Caching Strategy Checklist

Before caching something, ask yourself:
  • [ ] Is this read-heavy? (Cache read-heavy data)
  • [ ] Does it change often? (Don't cache if it changes every second)
  • [ ] Is it expensive to compute? (Cache expensive operations)
  • [ ] Can it be stale? (How old is too old?)
  • [ ] How much data? (Don't cache massive datasets)
  • [ ] How many users? (Cache matters more with scale)
Example decisions:
✅ Cache: User profiles (read-heavy, changes rarely) ✅ Cache: Product listings (stable, expensive DB query) ✅ Cache: Search results (very expensive, can be 5min old) ❌ Don't cache: Real-time stock prices (changes constantly) ❌ Don't cache: Personal financial data (too sensitive, must be fresh) ❌ Don't cache: Tiny queries (overhead > benefit)

⏰ TTL (Time To Live)

How long should you cache something?
1
2
3
4
5
6
7
8
// Short TTL: Data changes frequently
await redis.setex('trending-posts', 60, data); // 1 minute

// Medium TTL: Data changes occasionally
await redis.setex('user-profile', 3600, data); // 1 hour

// Long TTL: Data changes rarely
await redis.setex('countries-list', 86400 * 7, data); // 1 week
General guidelines:
  • Real-time data: 0-60 seconds
  • Frequently changing: 5-30 minutes
  • Stable data: 1-24 hours
  • Reference data: 1-4 weeks

🚨 Cache Problems & Solutions

Problem 1: Stale Data

User sees outdated information.
Solution: Active invalidation
1
2
3
4
5
// When data changes, invalidate cache immediately
async function updateProduct(id, data) {
await db.query('UPDATE products SET ? WHERE id = ?', [data, id]);
await redis.del(`product-${id}`); // Remove from cache
}

Problem 2: Cache Stampede

When cache expires, 1000 requests hit the database simultaneously.
Solution: Staggered expiration or locking
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function getPopularProduct(id) {
const cached = await redis.get(`product-${id}`);
if (cached) return JSON.parse(cached);

// Lock to prevent thundering herd
const lock = await redis.set(`lock-${id}`, '1', 'NX', 'EX', 5);

if (!lock) {
// Someone else is fetching, wait and try again
await sleep(100);
return getPopularProduct(id);
}

const product = await db.query('SELECT * FROM products WHERE id = ?', [id]);
await redis.setex(`product-${id}`, 3600, JSON.stringify(product));
await redis.del(`lock-${id}`);

return product;
}

Problem 3: Memory Explosion

Cache grows forever and crashes server.
Solution: Set maximum TTL and eviction policy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Redis eviction: remove oldest when full
redis.config('SET', 'maxmemory-policy', 'allkeys-lru');

// Or manually clean up
async function cleanupCache() {
const keys = await redis.keys('*');
const now = Date.now();

for (const key of keys) {
const ttl = await redis.ttl(key);
if (ttl < 0) { // No expiration set
await redis.del(key);
}
}
}

📊 Real-World Example

E-commerce product listing page:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
async function getProducts(filters) {
const cacheKey = `products-${JSON.stringify(filters)}`;

// Check cache
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}

// Build expensive query
let query = 'SELECT * FROM products WHERE 1=1';
const params = [];

if (filters.category) {
query += ' AND category = ?';
params.push(filters.category);
}
if (filters.minPrice) {
query += ' AND price >= ?';
params.push(filters.minPrice);
}
if (filters.maxPrice) {
query += ' AND price <= ?';
params.push(filters.maxPrice);
}

// Query database
const products = await db.query(query, params);

// Cache for 30 minutes (products don't change constantly)
await redis.setex(cacheKey, 1800, JSON.stringify(products));

return products;
}

// When product is updated, invalidate related caches
async function updateProduct(id, data) {
await db.query('UPDATE products SET ? WHERE id = ?', [data, id]);

// Clear all product listing caches (they might be affected)
const keys = await redis.keys('products-*');
for (const key of keys) {
await redis.del(key);
}
}

🎓 When to Cache (and When Not To)

Cache if:
  • ✅ Read-heavy operations
  • ✅ Expensive computations
  • ✅ Data doesn't change often
  • ✅ You have scale/traffic
Don't cache if:
  • ❌ Data changes constantly
  • ❌ Small queries (overhead > benefit)
  • ❌ Sensitive data (passwords, tokens)
  • ❌ Tiny dataset (just query DB)

💪 Performance Impact

Real-world numbers:
ScenarioWithout CacheWith CacheImprovement
Single DB query100ms5ms20x
Complex aggregation500ms10ms50x
Global request1000ms50ms20x
High traffic (1000 req/s)Database crashesHandles easily∞

Wrap-up

Caching is one of the highest ROI optimizations you can do:
✅ 10-50x faster ✅ 10-50x less database load ✅ Better user experience ✅ Lower infrastructure costs
Start simple (browser cache), move to Redis when you need it, use CDN for global scale.
Your users (and your database) will thank you.

P.S.: Cache invalidation is one of the two hardest problems in computer science (along with naming things). Don't feel bad if you get it wrong at first. We all do.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article