---
title: "Let one caller rebuild an expired entry · Agent Wranglers"
url: https://agentwranglers.se/playbook/scaling/caching/stampede-protection
updated: 2026-10-21
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Let one caller rebuild an expired entry

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Caching](https://agentwranglers.se/playbook/scaling/caching.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers). Updated 2026-10-21.

- ID: SCL-13
- Level: Recommended
- Chapter: [Caching](https://agentwranglers.se/playbook/scaling/caching)
- Rule: 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: 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](https://agentwranglers.se/playbook/scaling/stateless-app-servers/shared-locks-and-counters) shows, and serve the stale value meanwhile. Add up to 10% random time to each expiry, so entries written together do not expire together.
- 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.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### JavaScript example

```js
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

```python
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)
```

## Related rules

- [Keep locks and counters in a shared store](https://agentwranglers.se/playbook/scaling/stateless-app-servers/shared-locks-and-counters) in Scaling

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/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.

- Previous: [Put a format version in every cache key](https://agentwranglers.se/playbook/scaling/caching/versioned-cache-keys)
- Next: [Cap the cache server's memory and choose its eviction](https://agentwranglers.se/playbook/scaling/caching/cache-memory-and-eviction)
- [Caching](https://agentwranglers.se/playbook/scaling/caching)
