{"post":{"slug":"how-to-cache-with-redis-2026","title":"How to cache with Redis in 2026: patterns, TTLs, and invalidation","excerpt":"Redis caching in 2026 is faster to set up than ever, but the same mistakes persist: missing TTLs, stale data, and cache stampedes. This guide fixes all three.","category":"code","date":"March 11, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">R</span>edis is still the caching default in 2026, and its fundamental patterns haven't changed — but the mistakes teams make are consistent enough to be worth documenting. Missing TTLs fill memory until the node OOMs. No invalidation strategy serves stale data for hours. Stampedes on cache warm-up crush databases. This guide covers the right patterns.\n\n## What changed in 2026\n\n- **Redis 8** (released 2025) added a hash-based string encoding that reduces memory usage by ~10% for typical key sets, and improved replication throughput.\n- **Valkey** (the Redis fork from the Linux Foundation, post-license change) is available in most managed offerings (ElastiCache, Upstash) and is API-compatible.\n- **Dragonfly** is a Redis-compatible in-memory store with multi-threaded architecture; worth considering for single-node setups that need >100k ops/s.\n- **Upstash** (serverless Redis over HTTP) is the dominant choice for edge and serverless deployments where persistent TCP connections are impractical.\n\n## The cache-aside pattern\n\nCache-aside (lazy loading) is the right default. The application manages the cache explicitly:\n\n```ts\n// Node.js with ioredis\nimport { Redis } from 'ioredis';\nconst redis = new Redis(process.env.REDIS_URL!);\n\nasync function getUserProfile(userId: string) {\n  const key = `user:${userId}:profile`;\n\n  // 1. Try cache\n  const cached = await redis.get(key);\n  if (cached) return JSON.parse(cached);\n\n  // 2. Miss — fetch from DB\n  const user = await db.users.findUnique({ where: { id: userId } });\n  if (!user) return null;\n\n  // 3. Write to cache with TTL\n  await redis.set(key, JSON.stringify(user), 'EX', 300); // 5 minutes\n\n  return user;\n}\n```\n\nOn mutation:\n```ts\nasync function updateUserProfile(userId: string, data: Partial<User>) {\n  await db.users.update({ where: { id: userId }, data });\n  await redis.del(`user:${userId}:profile`); // invalidate\n}\n```\n\n## Key naming conventions\n\n```\n<entity>:<id>:<data-type>\n\nuser:123:profile\nuser:123:permissions\npost:456:body\nfeed:user:123:page:1\nrate_limit:ip:192.168.1.1\nsession:abc123\n```\n\nNever: `profile_123`, `userprofile123`, `UP_123`. Namespacing lets you:\n- Delete all keys for a user: `redis.del` with a collected list\n- Scan for a prefix: `SCAN 0 MATCH user:123:*` (avoid in production hot paths)\n- Reason about memory usage per entity type\n\n## TTL strategy\n\n| Data type | TTL | Reasoning |\n| --- | --- | --- |\n| User profile | 5–15 min | Changes infrequently; stale is tolerable briefly |\n| Session | 30 min (sliding) | Expire idle sessions |\n| Rate limit counters | 1 min (fixed) | Must be accurate; short window |\n| Rendered HTML pages | 60 s–10 min | High read, low write; stale is okay |\n| API response cache | 30–300 s | Depends on data freshness requirement |\n| Expensive DB query | 60–600 s | Match data change frequency |\n\nAdd ±10% jitter:\n```ts\nconst BASE_TTL = 300;\nconst jitter = Math.floor(Math.random() * BASE_TTL * 0.1);\nawait redis.set(key, value, 'EX', BASE_TTL + jitter);\n```\n\n## Preventing cache stampede\n\nWhen a popular key expires, many concurrent requests all miss and hammer the DB simultaneously.\n\n**Option 1: probabilistic early expiration**\n\n```ts\nasync function getCachedWithPER<T>(\n  key: string,\n  ttl: number,\n  fetch: () => Promise<T>,\n  beta = 1,\n): Promise<T> {\n  const raw = await redis.get(key);\n  if (raw) {\n    const { value, expireAt } = JSON.parse(raw) as { value: T; expireAt: number };\n    const remaining = (expireAt - Date.now()) / 1000;\n    // Probabilistically recompute before expiry\n    if (remaining > 0 && remaining > -beta * Math.log(Math.random())) {\n      return value;\n    }\n  }\n  const value = await fetch();\n  await redis.set(key, JSON.stringify({ value, expireAt: Date.now() + ttl * 1000 }), 'EX', ttl);\n  return value;\n}\n```\n\n**Option 2: mutex lock on miss**\n\n```ts\nconst lockKey = `lock:${key}`;\nconst acquired = await redis.set(lockKey, '1', 'NX', 'EX', 10);\nif (!acquired) {\n  await sleep(100); // brief wait for lock holder to populate\n  return getCached(key); // retry\n}\ntry {\n  const value = await fetch();\n  await redis.set(key, JSON.stringify(value), 'EX', ttl);\n  return value;\n} finally {\n  await redis.del(lockKey);\n}\n```\n\n## Invalidation patterns\n\n| Pattern | How | Best for |\n| --- | --- | --- |\n| Delete on write | `DEL key` after mutation | Simple, cache-aside |\n| Tag-based | Tag key with entity ID, delete by tag | Complex objects with shared data |\n| Write-through | Write to cache and DB in same step | Strongly consistent reads |\n| TTL-only | Just let it expire | Tolerant of short staleness |\n\nAvoid `FLUSHALL` or `FLUSHDB` in production — it clears everything including sessions and rate limit state.\n\n## Common mistakes\n\n**No TTL on cache writes.** Every `SET` must have an `EX` or `PX` option. One unbounded key is a memory leak.\n\n**Caching objects with circular references.** `JSON.stringify` throws; normalize to plain objects before caching.\n\n**Using `KEYS *` in production.** It blocks Redis while scanning. Use `SCAN` with a cursor for inspection.\n\n**Caching `null` as a miss.** A negative cache entry (caching \"this ID doesn't exist\") with a short TTL prevents repeated DB hits for missing resources. Not caching null means every missing-ID request hits the DB.\n\n**Storing large blobs.** Redis values above ~1 MB cause network and memory pressure. Compress or split large payloads, or store in object storage and cache only the URL.\n\n## What to skip\n\n- **Write-back caching** (write to cache only, async flush to DB) — the risk of data loss on cache node failure is rarely worth the write throughput gain for typical web apps.\n- **Caching deeply nested objects** that change partially — cache at the leaf level or use a tag-based strategy; updating a nested field invalidates the whole key.\n- **Redis as a primary database** — Redis persistence (RDB/AOF) is not a replacement for a relational DB; use it as a cache layer on top of Postgres.\n\n## FAQ\n\n**Redis or Valkey in 2026?**\nFor managed deployments (ElastiCache, Upstash), Valkey and Redis are API-identical. Pick based on your cloud provider's offering. For self-hosted, either is fine.\n\n**How do I cache in Python?**\nUse `redis-py` (sync) or `redis.asyncio` (async). Pattern is identical: get → miss → fetch DB → setex.\n\n**Should I use a Redis cluster for caching?**\nOnly if your dataset exceeds a single node's memory (~tens of GBs) or you need >500k ops/s. A single well-sized node handles most web app cache loads.\n\n**How do I debug what is in Redis?**\nUse `redis-cli --scan --pattern 'user:*'` to list keys by namespace, and `DEBUG OBJECT <key>` to inspect encoding and idle time. In production, use a tool like RedisInsight.\n\n## Where to go next\n\nSee [How to set up Redis caching in 2026](/blog/how-to-set-up-redis-caching-2026), [How to rate limit an API in 2026](/blog/how-to-rate-limit-an-api-2026), and [How to set up Postgres locally in 2026](/blog/how-to-set-up-postgres-locally-2026).","hue":307,"metaTitle":"How to Cache with Redis 2026: Patterns, TTLs, Invalidation","metaDescription":"Practical guide to Redis caching in 2026. Covers cache-aside pattern, TTL strategy, key namespacing, cache stampede prevention, and invalidation patterns.","keywords":["how to cache with redis 2026","redis caching patterns","cache aside pattern redis","redis ttl best practices","redis key namespacing","cache invalidation redis","redis thundering herd prevention","node redis caching","python redis cache","redis 8 caching 2026"],"tldr":["**Set a TTL on every cache key** — a key without expiry is a memory leak; choose TTL based on how stale the data can safely be, not convenience.","**Use cache-aside as the default pattern** — read from cache, miss → read DB, write to cache, return. Simple, debuggable, works for 90% of use cases.","**Namespace your keys** — `user:123:profile` beats `profile_123`; namespacing makes bulk invalidation and debugging possible.","**Add jitter to TTLs for hot keys** — identical TTLs expire together and cause a thundering herd; add `±10%` random jitter to spread expiry.","**Skip:** caching mutable data without an invalidation strategy. Stale cache serving wrong data is worse than a slow cache miss."],"readTime":"5 min read","wordCount":1076},"related":[{"slug":"how-to-set-up-redis-caching-2026","title":"How to set up Redis caching in 2026","excerpt":"Redis caching cuts database load and API latency dramatically. This guide covers cache-aside, TTL strategy, invalidation, and Upstash for serverless in 2026.","category":"code","date":"April 18, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":312},{"slug":"caching-strategies-2026","title":"Caching strategies in 2026: which pattern to use and when","excerpt":"Cache-aside, write-through, read-through, and CDN edge caching solve different problems. Here is the 2026 decision guide with concrete tradeoffs.","category":"code","date":"April 13, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":322},{"slug":"how-to-cache-api-responses-2026","title":"How to cache API responses in 2026: HTTP headers, Redis, and CDN strategies","excerpt":"Caching is the single fastest way to reduce API latency and database load. This is the 2026 guide to HTTP caching, in-process caches, Redis, and CDN layers.","category":"code","date":"March 29, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":315}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-bundle-insurance-2026","title":"How to bundle insurance in 2026: save without cutting the wrong coverage","excerpt":"Bundling auto and home insurance with one carrier can cut premiums by 10–25%. Here is how to do it right in 2026 without accidentally reducing coverage.","category":"finance","date":"March 11, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":306},"older":{"slug":"how-to-calculate-a-mortgage-payment-2026","title":"How to calculate a mortgage payment in 2026: the exact formula","excerpt":"Knowing what your mortgage payment will be before you apply saves surprises. Here is the exact calculation method and every cost you need to include.","category":"finance","date":"March 11, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":284}}