Spreading expiries with TTL jitter

Caching and Redis Mid-level Python 3.13+ PostgreSQL 17+ Reviewed By Best Latest Apps

Predict the output

Without jitter, how many of the 1,000 keys expire in the busiest second?

Python 3.13
ttls = [60 for _ in range(1000)]
print(max(ttls.count(t) for t in set(ttls)))

Options
Hint 1 of 1

How many distinct TTL values are there?

Choose an answer, then open Show answer below to check it.

1,000 keys cached at the same moment. With a fixed 60-second TTL all 1,000 expire in one second. With a random 55 to 65 seconds, expiries spread over 11 seconds and the busiest second has about a tenth.

Run on Redis 7

Text version
Fixed TTL: peak 1,000 in one second, over 1 second
SecondKeys expiring
601,000
Jittered TTL: peak 110 in one second, over 11 seconds
SecondKeys expiring
5582
5699
5777
58110
59102
6074
6192
6289
6391
6489
6595
Show answer

Answer: A 1000

Correct. They were written together with one TTL, so they all expire together.

Every key got the same 60-second TTL at the same time, so they all expire in the same second. Adding a small random offset to each TTL spreads the expiries out.

Why not the others?
  • B 17. That's roughly 1000 / 60, as if expiries spread across the minute.
  • C 100. That's closer to the jittered peak.
  • D 1. Every key has the same TTL.

30-second answer

They all expire in the same second, so the next minute starts with 1,000 misses hitting the database together. That's a different problem from one hot key: many keys share an expiry time. Add jitter: give each key a TTL with a random spread, for example 55 to 65 seconds. In our Redis run, the busiest second dropped from 1,000 expiries to about a tenth of that.

Practise saying it · 25 s. No recording, just a timer

Why add TTL jitter?

Say your answer out loud, then check which of these points you covered:

  • Keys written together expire together
  • Spread with random TTL
  • ±10% of TTL
  • Not the hot-key fix

Deep dive

Jitter helps after cache warm-ups, deploys and bulk imports, anything that writes many keys together.

Spread by a percentage of the TTL (±10%) rather than a fixed number of seconds.

For one hot key, jitter does nothing; use single-flight or stale-while-revalidate.

Seed nothing in production; a fixed seed here only makes the demo repeatable.

1,000 keys, fixed vs jittered TTL, read back with EXPIRETIME Ran on Python 3.13.5 · mypy --strict
Python 3.13
import random
from collections import Counter

import redis

r = redis.Redis(decode_responses=True)
BASE = 2_000_000_000  # a fixed unix time, so the run is repeatable


def spread(jitter: bool) -> Counter[int]:
    r.flushall()
    rng = random.Random(42)
    pipe = r.pipeline()
    for i in range(1000):
        ttl = 55 + rng.randint(0, 10) if jitter else 60
        pipe.set(f"product:{i}", "{}")
        pipe.expireat(f"product:{i}", BASE + ttl)
    pipe.execute()
    at = Counter[int]()
    for i in range(1000):
        at[int(r.expiretime(f"product:{i}")) - BASE] += 1
    return at


for name, jitter in (("Fixed TTL", False), ("Jittered TTL", True)):
    counts = spread(jitter)
    print(f"{name}: {max(counts.values())}")
    print("  seconds:", sorted(counts.items()))

Common mistakes

  • Using the same TTL for everything written by a batch job.
  • Adding jitter to fix a single hot key's stampede.
  • Jitter so large that some data stays far too stale.

Senior take & follow-ups

Explain synchronised expiry, the jitter fix, and that it's not the hot-key fix.

Follow-up questions

Think of your answer first, then open the hint.

  1. How much jitter?

    Show hint

    Often ±10% of the TTL; enough to flatten the peak, small enough to bound staleness.

  2. Where else does synchronised timing hurt?

    Show hint

    Cron jobs at :00, retries without jitter, client polling.

Suggest a fix if something here is wrong or out of date.