LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Caching Strategies for Performance

cachingredisperformanceoptimization

Caching is the most effective way to improve performance. Here’s how to do it right.

Cache Layers

Client → CDN Cache → App Cache → Database Cache → Database

Each layer reduces load on the next.

Common Patterns

Cache-Aside (Lazy Loading)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def get_user(user_id):
    # Check cache
    cached = redis.get(f"user:{user_id}")
    if cached:
        return json.loads(cached)

    # Load from database
    user = db.query("SELECT * FROM users WHERE id = ?", user_id)

    # Store in cache
    redis.setex(f"user:{user_id}", 3600, json.dumps(user))
    return user

Pros: Only caches what’s needed Cons: Cache miss penalty, potential stale data

Write-Through

1
2
3
4
5
6
def update_user(user_id, data):
    # Update database
    db.query("UPDATE users SET ... WHERE id = ?", user_id)

    # Update cache immediately
    redis.setex(f"user:{user_id}", 3600, json.dumps(data))

Pros: Cache always consistent Cons: Write latency, unused data cached

Write-Behind

1
2
3
4
5
6
def update_user(user_id, data):
    # Update cache immediately
    redis.setex(f"user:{user_id}", 3600, json.dumps(data))

    # Queue database update
    queue.publish("user_updates", {"id": user_id, "data": data})

Pros: Fast writes Cons: Complexity, potential data loss

Cache Invalidation

The two hard things in computer science:

  1. Cache invalidation
  2. Naming things

Time-Based (TTL)

1
redis.setex("user:123", 3600, data)  # Expires in 1 hour

Simple but may serve stale data.

Event-Based

1
2
3
def update_user(user_id, data):
    db.update(user_id, data)
    redis.delete(f"user:{user_id}")  # Invalidate

More accurate but more complex.

Versioned Keys

1
2
version = redis.incr("user:123:version")
redis.set(f"user:123:v{version}", data)

What to Cache

Good candidates:

  • Expensive computations
  • Frequently accessed data
  • Rarely changing data
  • External API responses

Poor candidates:

  • Rapidly changing data
  • User-specific data (low hit rate)
  • Large objects

Redis Examples

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import redis

r = redis.Redis(host='localhost', port=6379)

# String
r.set("key", "value")
r.get("key")

# Hash (for objects)
r.hset("user:123", mapping={"name": "John", "email": "john@example.com"})
r.hgetall("user:123")

# List (for queues)
r.lpush("tasks", "task1")
r.rpop("tasks")

# Set (for unique items)
r.sadd("online_users", "user1", "user2")
r.smembers("online_users")

# Sorted Set (for leaderboards)
r.zadd("scores", {"player1": 100, "player2": 200})
r.zrevrange("scores", 0, 10, withscores=True)

Cache Metrics

Track these:

  • Hit rate: % of requests served from cache
  • Miss rate: % requiring database
  • Eviction rate: Items removed before TTL
  • Memory usage: Cache size
1
2
3
4
5
# Redis INFO
info = redis.info()
hits = info['keyspace_hits']
misses = info['keyspace_misses']
hit_rate = hits / (hits + misses)

Common Mistakes

  1. No TTL: Cache grows forever
  2. Cache stampede: Many requests miss simultaneously
  3. Over-caching: Caching cheap operations
  4. Ignoring consistency: Stale data causes bugs

Cache Stampede Prevention

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def get_with_lock(key, compute_fn, ttl=3600):
    cached = redis.get(key)
    if cached:
        return cached

    # Try to acquire lock
    lock_key = f"{key}:lock"
    if redis.setnx(lock_key, "1"):
        redis.expire(lock_key, 30)
        value = compute_fn()
        redis.setex(key, ttl, value)
        redis.delete(lock_key)
        return value
    else:
        # Wait and retry
        time.sleep(0.1)
        return get_with_lock(key, compute_fn, ttl)

Caching is powerful but requires careful design. Start simple, measure, and optimize.

Comments