New:Upstash has a remote MCP server

How TTL Jitter Protects Your Database When Redis Keys Expire Together

JoshJosh
4 min read

We loaded 5,000 keys into a real Upstash Redis database in one batch, each with a TTL of 30 seconds (time to live, after which Redis deletes the key), and then read them at 2,000 reads per second. For 29 seconds, nothing missed. Then at second 30, 1,683 reads missed in one second, and each of them went to the database behind the cache.

Adding 10% of random jitter to the TTLs cut that peak to 779 misses per second, so the database behind the cache saw less than half the queries at once. The total stayed about the same, at 10,105 misses without jitter and 10,115 with it, so the same work spread out over more seconds.

The test used cache-aside, where a miss waits 50 ms for a stand-in database query and writes the value back with a new TTL. The client was about 1 ms from a Pay as You Go database in AWS us-east-1, and it picked each key at random.

How does TTL jitter help performance?

With 10% TTL jitter, the peak load on the database behind the cache went down by 54%. The total number of cache misses was about the same (because the keys still expired and the next read missed), but because the expirations were spread out, the peak database load went down.

This is what changed with each jitter width:

TTL for a 30 second keyPeak misses per secondPeak database queries at onceTotal missesMiss rate
Fixed at 30 s1,68311710,1056.55%
10% jitter, 27 to 33 s7795410,1156.56%
25% jitter, 22.5 to 37.5 s355319,8156.36%
50% jitter, 15 to 45 s238229,3826.07%

The wider the jitter, the smaller the peak. At 50%, the database behind the cache saw 22 queries at once instead of 117.

These are misses per second around the first expiry, one sample every two seconds:

second    fixed    10%    25%    50%
    16        0      0      0     71
    20        0      0      0    166
    24        0      0    205    166
    28        0    367    305    149
    30     1683    668    324    170
    32      769    779    314    171
    34      311    415    339    192
    36      130    180    343    189
    40       22     36     88    201
    44        5      9     25    238

The wave comes back one TTL later

Every key that missed in the first wave got rewritten with a new TTL, so those keys expire together again. Without jitter, misses fell to almost zero by second 50, then climbed again from second 60 to a peak of 796.

This second wave is lower and wider than the first, because reads refill keys at random over several seconds. Jitter lowers this one too:

TTL for a 30 second keyFirst peakSecond peak
Fixed at 30 s1,683796
10% jitter779542
25% jitter355290
50% jitter238202

How much jitter should you use?

Start with 10%. It cut the peak by 54% in our test, and the data only lives 10% longer than planned in the worst case. Go wider when the database behind the cache can't take the peak.

The cost of a wider spread is staleness. With 50% jitter, a key with a 30 second TTL can stay for 45 seconds, so that spread only suits data that can be that old. With a 300 second TTL, 10% means 270 to 330 seconds.

This code sets the jitter in milliseconds, because with a short TTL, rounding to whole seconds can remove it:

import { Redis } from "@upstash/redis";
 
const redis = Redis.fromEnv();
 
function jitteredTtlMs(baseSeconds: number, spread = 0.1) {
  return Math.round(baseSeconds * 1000 * (1 + (Math.random() * 2 - 1) * spread));
}
 
const pipeline = redis.pipeline();
for (let i = 0; i < 5; i++) pipeline.set(`product:${i}`, "cached", { px: jitteredTtlMs(300) });
await pipeline.exec();
 
const ttls = await Promise.all([0, 1, 2, 3, 4].map((i) => redis.ttl(`product:${i}`)));
console.log(ttls);
[ 324, 318, 304, 301, 297 ]

Where jitter helps most

Jitter helps most when many keys share a TTL and get written together, such as a bulk load, a cache warmer or a deploy that refills the cache. When few keys share a TTL, there's no need to worry about it.

A single hot key expires at one moment whatever its TTL, so it needs a lock, request coalescing or stale-while-revalidate. These combine well with jitter.

Our numbers show what the client and the database behind the cache see. We didn't measure the work Redis does to expire keys. On stock Redis that work runs on the main thread, so a large wave costs CPU too.

What to use

  • Lock or stale-while-revalidate for the one-hot-key scenario.
  • Extend the TTL to lower the miss rate.
  • Set jitter in milliseconds if the TTL is short.
  • Use 10% jitter on bulk loads of many keys. Jitter should be higher if the database behind the cache cannot handle the peak.