Agent Wranglers

SCL-13Recommended

Let one caller rebuild an expired entry

When a hot entry expires, let one caller rebuild it while the others wait for that result or get the old value. Add random jitter to every expiry time.

How to do it

Without this, every request that misses at the same moment runs the same slow query. The database then takes the full load just as the cache empties. Within one process, share the pending promise or future for each key. Across copies, take a short lock per key as Keep locks and counters in a shared store shows, and serve the stale value meanwhile. Add up to 10% random time to each expiry, so entries written together do not expire together.

Examples

JavaScript example

const pending = new Map();
const JITTER_SHARE = 0.1;

export async function cached(cache, key, ttlSeconds, load) {
  const hit = await cache.get(key);
  if (hit !== null) return JSON.parse(hit);
  if (!pending.has(key)) {
    const ttl = Math.round(ttlSeconds * (1 + Math.random() * JITTER_SHARE));
    pending.set(key, load()
      .then(async value => { await cache.set(key, JSON.stringify(value), 'EX', ttl); return value; })
      .finally(() => pending.delete(key)));
  }
  return pending.get(key);
}

Python example

import asyncio
import json
import random

JITTER_SHARE = 0.1
_pending: dict[str, asyncio.Future] = {}

async def cached(cache, key: str, ttl_seconds: int, load):
    hit = await cache.get(key)
    if hit is not None:
        return json.loads(hit)
    if key not in _pending:
        _pending[key] = asyncio.ensure_future(_rebuild(cache, key, ttl_seconds, load))
    return await _pending[key]

async def _rebuild(cache, key, ttl_seconds, load):
    try:
        value = await load()
        ttl = round(ttl_seconds * (1 + random.random() * JITTER_SHARE))
        await cache.set(key, json.dumps(value), ex=ttl)
        return value
    finally:
        _pending.pop(key, None)

How to check

In a test, expire a hot key while 500 requests read it at once. The source is called at most once per app copy, and no request fails.

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/scaling/caching/stampede-protection">Agent Wranglers scaling playbook</a>. In code you write, add a comment next to it with the same link.