Infrastructure

Redis Caching in 2026: Patterns, Eviction Policies, and When Not to Use It

May 2026 · 10 min read

Verdict up front: Most apps that add Redis don't need it. A 10MB in-process LRU cache (Python functools.lru_cache, Node lru-cache) gets you 80% of the benefit at 0% of the operational cost. Reach for Redis only when (a) you need to share cache across multiple instances, (b) your working set is larger than per-instance memory, or (c) you need pub/sub or distributed locks.

Why "cache it in Redis" is the default and often wrong

The reflex to add Redis comes from web tutorials that assume distributed workloads. Most real apps run 2-5 instances, share-nothing, and would be fine with local memory.

Latency comparison for a 1KB lookup:

  • L1 cache hit (CPU): 1ns
  • In-process dict / map: 100ns
  • Local Redis (Unix socket): 50μs
  • Redis over network (same AZ): 500μs
  • Redis over network (cross-region): 50ms+

An in-process cache is 5,000x faster than network Redis. The only reasons to give that up are sharing across instances or working sets bigger than per-instance RAM.

Eviction policies, plainly

  • noeviction — write fails when memory is full. Default. Use for queues, never for caches.
  • allkeys-lru — evict least-recently-used across all keys. Use this for general-purpose caches.
  • allkeys-lfu — evict least-frequently-used. Better when access pattern is skewed (10% of keys get 90% of reads).
  • allkeys-random — evict random keys. Faster than LRU/LFU, useful when access pattern is uniform.
  • volatile-lru — evict LRU among keys with TTL only. Use when you mix cache and durable data in one Redis.
  • volatile-lfu — like above but LFU.
  • volatile-ttl — evict keys closest to expiration first.

Default for new Redis instances: maxmemory-policy allkeys-lru. This is the single most common misconfiguration — leaving it on noeviction and watching writes fail at the worst moment.

Cache-aside (lazy loading)

The pattern that fits 90% of use cases:

def get_user(user_id):
    cached = redis.get(f"user:{user_id}")
    if cached:
        return json.loads(cached)
    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    redis.setex(f"user:{user_id}", 300, json.dumps(user))  # 5 min TTL
    return user

Properties:

  • Read path tolerates Redis being down (falls through to DB)
  • Cache populated on demand, no warm-up
  • Stale reads possible for up to TTL (acceptable for most data)
  • Cache stampede risk: 100 concurrent requests for a cold key all hit the DB. Mitigate with a 1-key lock or probabilistic early expiration.

Write-through

Every write updates Redis and DB atomically (or with retry/rollback). Used when stale reads aren't tolerable.

def update_user(user_id, data):
    db.update("UPDATE users SET ... WHERE id = %s", user_id)
    redis.setex(f"user:{user_id}", 3600, json.dumps(data))

Risk: DB write succeeds, Redis write fails — cache now stale. Use Redis pipelines + transactions to make this atomic, or use cache-aside with a shorter TTL.

Write-behind (write-back)

Writes go to Redis, then async to DB. Higher throughput, risk of data loss on crash.

Rarely needed in 2026. Only justified when DB write latency is the bottleneck and you can tolerate losing the last few seconds of writes. Most teams that try this regret it within 6 months.

TTL strategy

Common mistakes:

  • TTL too short: 30 seconds means every page load is a cache miss. You're paying Redis bills for nothing.
  • TTL too long: 24 hours means data changes don't propagate. Users see stale prices, stale notifications.
  • Same TTL for everything: User profiles (slow-changing) and live order status (fast-changing) should not share a TTL.

Rule of thumb: TTL = (1 / change frequency) × 0.5. If user profiles change once a week, cache for ~3 days. If prices change every 5 minutes, cache for ~90 seconds.

Cache hit ratio: what's good?

  • <50% hit ratio: Redis is hurting you, not helping. Latency + cost without benefit.
  • 50-80%: Acceptable, but tune TTL or eviction policy.
  • >90%: Healthy. Don't optimise further.

Check with: redis-cli INFO stats | grep keyspace → look at keyspace_hits / (keyspace_hits + keyspace_misses).

When Redis isn't the right tool

Use caseBetter option
Cache for a single-instance appLocal LRU (functools.lru_cache, lru-cache npm)
Persistent message queueRabbitMQ, AWS SQS, Cloudflare Queues
Full-text searchPostgres tsvector, Meilisearch, ElasticSearch
Time-series dataTimescaleDB, InfluxDB, ClickHouse
Cross-region cacheCloudflare KV, DynamoDB Global Tables
Counter increments onlyPostgres + UPSERT works fine to ~10k/sec

Pricing

  • Self-hosted on a DO droplet: €6/month for 1GB Redis
  • AWS ElastiCache (cache.t4g.micro): $16/month
  • Upstash serverless: €0 + per-request ($0.20 per 100k commands, 256MB free)
  • Redis Cloud (free tier): 30MB free, $7/month for 250MB

For low-volume side projects: Upstash. For production with consistent load: ElastiCache or self-hosted. For learning: a 1GB Docker container locally.

Cache layer not paying off?

We audit Redis usage and find the 80% of keys that shouldn't be in Redis at all — either move to local cache or skip caching entirely. Typical bill cut: 40-60%.

Book a discovery call

Related Posts

PostgreSQL vs MySQLCloud Cost OptimizationSaaS Infrastructure Cost Guide
← All blog posts

Need a caching layer that actually helps?

Free 20-minute call to look at your cache hit ratio, eviction patterns, and whether you need Redis at all. Sometimes the answer is a 10MB in-process LRU.

Book a discovery call