Skip to content
About
Main content

Measure leverage and the cost of a cold restart

Simulate a modelNeeds Python with NumPy

Problem

A in front of a database answers a share h of the reads (the ), so the database sees only the misses, a share 1 − h. The database is then sized for that remainder. That makes the cache's hit rate a lever on the layer below: the database's load is multiplied by 1/(1 − h) if the cache stops answering. At h = 0.95 the database is sized for one twentieth of the traffic, so a restart that empties the cache sends it twenty times its normal load until the cache refills.

See that hit rate is leverage, and that a restart is a load spike on the layer below.

  1. Run Postgres (or SQLite) with a table of 100,000 rows. Write a Python loop that reads keys drawn from a (numpy.random.zipf(1.2) modulo 100,000) and counts database reads.
  2. Put a dictionary cache with eviction (collections.OrderedDict) in front. Use capacity 5,000 entries. Record hit rate h and database reads per 10,000 requests.
  3. Compute leverage 1/(1 − h). Then clear the cache in the middle of the run and print database reads per second for the next 60 seconds.

The script uses SQLite and a steady 2,000 requests per second, counted in 100 ms ticks, so you can see the spike and how fast the cache refills.

The question. What hit rate does this cache reach, and how big is the database's load spike right after the cache is cleared, compared with 1/(1 − h)?


Predict

Right after the cache is cleared, how many times its warm load does the database see at peak?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    measure-leverage-and-the-cost-of.py85 linesDownload
    View the code
    measure-leverage-and-the-cost-of.py
    # /// script
    # requires-python = ">=3.10"
    # dependencies = ["numpy"]
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """Measure leverage and the cost of a cold restart.
    
    A Zipf-distributed read stream against SQLite, first bare, then behind an LRU
    dictionary cache; then the cache is cleared mid-run and the database load is
    followed second by second at a steady request rate.
    """
    import argparse, json, os, sqlite3, tempfile
    from collections import OrderedDict
    import numpy as np
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--rows", type=int, default=100_000, help="Rows in the table (and distinct keys).")
    p.add_argument("--zipf", type=float, default=1.2, help="Zipf exponent of the key stream (numpy.random.zipf, modulo rows).")
    p.add_argument("--capacity", type=int, default=5_000, help="Cache capacity, in entries (LRU).")
    p.add_argument("--rate", type=int, default=2_000, help="Requests per second offered to the cache (the clock is the request count).")
    p.add_argument("--warm-seconds", type=int, default=30, help="Seconds of traffic before the cache is cleared.")
    p.add_argument("--after-seconds", type=int, default=60, help="Seconds of traffic followed after the clear.")
    p.add_argument("--seed", type=int, default=0, help="Random seed.")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    rng = np.random.default_rng(args.seed)
    
    # 1. The database: SQLite in a temporary file, one row per key
    path = os.path.join(tempfile.gettempdir(), "leverage-lab.db")
    import atexit; atexit.register(lambda: os.path.exists(path) and os.remove(path))   # leave nothing behind
    if os.path.exists(path): os.remove(path)
    db = sqlite3.connect(path)
    db.execute("CREATE TABLE kv (k INTEGER PRIMARY KEY, v TEXT)")
    db.executemany("INSERT INTO kv VALUES (?, ?)", ((k, f"value-{k}") for k in range(args.rows)))
    db.commit()
    db_reads = 0
    def read_db(k):
        global db_reads
        db_reads += 1
        return db.execute("SELECT v FROM kv WHERE k = ?", (k,)).fetchone()[0]
    
    def keys(n):
        return (rng.zipf(args.zipf, n) % args.rows).tolist()
    
    # 2. An LRU dictionary cache in front
    cache = OrderedDict()
    def get(k):
        if k in cache:
            cache.move_to_end(k); return cache[k]
        v = read_db(k)
        cache[k] = v
        if len(cache) > args.capacity: cache.popitem(last=False)
        return v
    
    # 3. Steady traffic in 100 ms ticks: database reads per second, the cache cleared after warm-seconds
    TICK = 10                                               # ticks per second
    per_tick = args.rate // TICK
    timeline = []
    for tick in range((args.warm_seconds + args.after_seconds) * TICK):
        if tick == args.warm_seconds * TICK:
            cache.clear()
        before = db_reads
        for k in keys(per_tick): get(k)
        timeline.append({"second": (tick - args.warm_seconds * TICK) / TICK, "db_reads_per_s": (db_reads - before) * TICK})
    
    warm = [t["db_reads_per_s"] for t in timeline if -10 <= t["second"] < 0]   # the last 10 s before the clear
    steady_reads = sum(warm) / len(warm)
    h = 1 - steady_reads / args.rate
    leverage = 1 / (1 - h)
    peak = max(t["db_reads_per_s"] for t in timeline if t["second"] >= 0)
    recovered = next(t["second"] for t in timeline if t["second"] >= 0 and t["db_reads_per_s"] <= 1.5 * steady_reads)
    print(f"hit rate h = {h:.3f}  ->  leverage 1/(1-h) = {leverage:.1f}")
    print(f"database reads per 10,000 requests, warm: {steady_reads / args.rate * 10_000:,.0f} (without the cache: 10,000)")
    print(f"after the clear: peak {peak:,.0f} reads/s against {steady_reads:,.0f} reads/s warm = {peak / steady_reads:.1f}x "
          f"(1/(1-h) = {leverage:.1f}); back under 1.5x warm after {recovered:.1f} s")
    for t in [t for t in timeline if 0 <= t["second"] < 1]:
        print(f"  {t['second']:.1f} s after clear: {t['db_reads_per_s']:>6,.0f} reads/s")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"timeline": timeline, "summary": [{
                "h": h, "leverage": leverage, "steady_reads": steady_reads, "peak_reads": peak,
                "spike": peak / steady_reads, "rate": args.rate, "recovered_s": recovered}]}}, f)
    
  3. 3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.

    Terminal
    uv run measure-leverage-and-the-cost-of.py
    uv run measure-leverage-and-the-cost-of.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--rows100,000 Rows in the table (and distinct keys).
--zipf1.2 Zipf exponent of the key stream (numpy.random.zipf, modulo rows).
--capacity5000 Cache capacity, in entries (LRU).
--rate2000 Requests per second offered to the cache (the clock is the request count).
--warm-seconds30 Seconds of traffic before the cache is cleared.
--after-seconds60 Seconds of traffic followed after the clear.
--seed0 Random seed.

Our run

Database reads per second, cache cleared at 0 sApple M2 · 8 cores · 8 GB
Data table
Database reads per second, cache cleared at 0 s
Seconds from the clearDatabase reads / s
-301,320
-29.9910
-29.8900
-29.7550
-29.6610
-29.5670
-29.4540
-29.3710
-29.2720
-29.1610
-29540
-28.9530
-28.8590
-28.7530
-28.6650
-28.5540
-28.4660
-28.3570
-28.2650
-28.1630
-28490
-27.9590
-27.8660
-27.7420
-27.6500
-27.5520
-27.4620
-27.3500
-27.2520
-27.1500
-27480
-26.9580
-26.8480
-26.7630
-26.6490
-26.5490
-26.4420
-26.3510
-26.2470
-26.1370
-26460
-25.9490
-25.8490
-25.7480
-25.6560
-25.5460
-25.4450
-25.3530
-25.2610
-25.1450
-25650
-24.9450
-24.8400
-24.7490
-24.6510
-24.5500
-24.4450
-24.3390
-24.2560
-24.1480
-24420
-23.9440
-23.8470
-23.7360
-23.6450
-23.5480
-23.4390
-23.3470
-23.2440
-23.1420
-23350
-22.9400
-22.8390
-22.7440
-22.6510
-22.5420
-22.4530
-22.3410
-22.2460
-22.1350
-22400
-21.9310
-21.8470
-21.7380
-21.6580
-21.5360
-21.4420
-21.3480
-21.2440
-21.1450
-21440
-20.9400
-20.8380
-20.7490
-20.6510
-20.5470
-20.4430
-20.3390
-20.2390
-20.1360
-20360
-19.9390
-19.8350
-19.7350
-19.6440
-19.5310
-19.4400
-19.3450
-19.2320
-19.1380
-19500
-18.9370
-18.8450
-18.7320
-18.6320
-18.5420
-18.4310
-18.3470
-18.2470
-18.1380
-18430
-17.9390
-17.8360
-17.7350
-17.6360
-17.5390
-17.4430
-17.3380
-17.2540
-17.1420
-17360
-16.9460
-16.8430
-16.7450
-16.6380
-16.5450
-16.4500
-16.3450
-16.2330
-16.1400
-16320
-15.9360
-15.8390
-15.7390
-15.6450
-15.5430
-15.4360
-15.3340
-15.2350
-15.1470
-15510
-14.9370
-14.8350
-14.7480
-14.6440
-14.5430
-14.4320
-14.3470
-14.2310
-14.1490
-14360
-13.9390
-13.8410
-13.7370
-13.6410
-13.5420
-13.4400
-13.3330
-13.2450
-13.1460
-13410
-12.9370
-12.8320
-12.7440
-12.6390
-12.5520
-12.4490
-12.3350
-12.2420
-12.1370
-12360
-11.9420
-11.8370
-11.7440
-11.6470
-11.5430
-11.4360
-11.3440
-11.2450
-11.1410
-11400
-10.9470
-10.8400
-10.7390
-10.6430
-10.5400
-10.4460
-10.3410
-10.2410
-10.1410
-10380
-9.9400
-9.8320
-9.7370
-9.6400
-9.5460
-9.4350
-9.3360
-9.2350
-9.1450
-9450
-8.9430
-8.8360
-8.7350
-8.6350
-8.5380
-8.4370
-8.3400
-8.2320
-8.1340
-8380
-7.9450
-7.8320
-7.7410
-7.6450
-7.5520
-7.4460
-7.3420
-7.2350
-7.1410
-7330
-6.9460
-6.8380
-6.7370
-6.6420
-6.5370
-6.4490
-6.3450
-6.2410
-6.1400
-6450
-5.9400
-5.8290
-5.7420
-5.6440
-5.5340
-5.4400
-5.3400
-5.2470
-5.1410
-5410
-4.9410
-4.8400
-4.7450
-4.6340
-4.5410
-4.4330
-4.3440
-4.2320
-4.1350
-4380
-3.9430
-3.8440
-3.7500
-3.6450
-3.5370
-3.4380
-3.3330
-3.2420
-3.1330
-3410
-2.9390
-2.8400
-2.7360
-2.6350
-2.5420
-2.4470
-2.3320
-2.2370
-2.1500
-2380
-1.9360
-1.8430
-1.7360
-1.6280
-1.5420
-1.4410
-1.3360
-1.2400
-1.1320
-1520
-0.9310
-0.8330
-0.7360
-0.6350
-0.5410
-0.4370
-0.3460
-0.2440
-0.1300
01,140
0.1860
0.2820
0.3690
0.4650
0.5830
0.6470
0.7740
0.8610
0.9670
1630
1.1630
1.2690
1.3570
1.4600
1.5560
1.6660
1.7620
1.8430
1.9580
2470
2.1530
2.2520
2.3540
2.4580
2.5610
2.6520
2.7560
2.8590
2.9480
3480
3.1430
3.2470
3.3510
3.4450
3.5560
3.6460
3.7440
3.8470
3.9440
4410
4.1520
4.2440
4.3510
4.4550
4.5510
4.6400
4.7550
4.8440
4.9360
5390
5.1410
5.2470
5.3450
5.4490
5.5360
5.6380
5.7420
5.8470
5.9410
6510
6.1470
6.2480
6.3390
6.4460
6.5460
6.6510
6.7390
6.8500
6.9480
7520
7.1380
7.2440
7.3390
7.4540
7.5320
7.6410
7.7330
7.8510
7.9500
8440
8.1410
8.2410
8.3350
8.4530
8.5490
8.6440
8.7420
8.8430
8.9450
9400
9.1470
9.2480
9.3390
9.4520
9.5450
9.6440
9.7370
9.8510
9.9420
10430
10.1400
10.2400
10.3400
10.4470
10.5440
10.6390
10.7380
10.8400
10.9370
11330
11.1390
11.2330
11.3380
11.4350
11.5510
11.6420
11.7510
11.8380
11.9460
12410
12.1420
12.2470
12.3420
12.4430
12.5440
12.6440
12.7390
12.8490
12.9540
13360
13.1350
13.2520
13.3350
13.4480
13.5520
13.6460
13.7430
13.8320
13.9450
14390
14.1540
14.2340
14.3530
14.4400
14.5410
14.6420
14.7390
14.8480
14.9420
15420
15.1450
15.2510
15.3360
15.4440
15.5470
15.6430
15.7420
15.8380
15.9370
16350
16.1420
16.2330
16.3460
16.4380
16.5320
16.6450
16.7350
16.8400
16.9340
17370
17.1320
17.2460
17.3390
17.4320
17.5390
17.6280
17.7500
17.8480
17.9400
18420
18.1440
18.2430
18.3490
18.4350
18.5440
18.6310
18.7440
18.8300
18.9370
19400
19.1360
19.2330
19.3400
19.4380
19.5350
19.6430
19.7410
19.8500
19.9380
20510
20.1410
20.2410
20.3340
20.4280
20.5340
20.6330
20.7470
20.8380
20.9430
21350
21.1470
21.2400
21.3430
21.4340
21.5440
21.6440
21.7420
21.8410
21.9430
22410
22.1400
22.2360
22.3390
22.4420
22.5430
22.6400
22.7370
22.8420
22.9370
23400
23.1370
23.2410
23.3480
23.4370
23.5330
23.6470
23.7410
23.8370
23.9400
24340
24.1470
24.2370
24.3370
24.4380
24.5440
24.6430
24.7490
24.8420
24.9270
25320
25.1400
25.2430
25.3490
25.4470
25.5460
25.6440
25.7460
25.8310
25.9430
26500
26.1360
26.2260
26.3340
26.4420
26.5470
26.6480
26.7470
26.8500
26.9490
27320
27.1350
27.2370
27.3360
27.4380
27.5470
27.6330
27.7380
27.8500
27.9410
28360
28.1510
28.2500
28.3410
28.4380
28.5450
28.6440
28.7390
28.8430
28.9430
29360
29.1490
29.2510
29.3380
29.4440
29.5380
29.6440
29.7400
29.8440
29.9380
30400
30.1500
30.2500
30.3490
30.4330
30.5360
30.6370
30.7550
30.8420
30.9560
31380
31.1390
31.2500
31.3400
31.4440
31.5400
31.6440
31.7420
31.8400
31.9380
32550
32.1480
32.2380
32.3380
32.4420
32.5520
32.6430
32.7420
32.8290
32.9380
33510
33.1340
33.2510
33.3390
33.4530
33.5320
33.6410
33.7440
33.8320
33.9360
34380
34.1490
34.2420
34.3390
34.4400
34.5370
34.6520
34.7360
34.8470
34.9390
35520
35.1510
35.2440
35.3470
35.4360
35.5420
35.6370
35.7440
35.8390
35.9430
36300
36.1410
36.2370
36.3410
36.4470
36.5400
36.6390
36.7430
36.8390
36.9440
37500
37.1380
37.2480
37.3440
37.4430
37.5470
37.6460
37.7330
37.8390
37.9440
38460
38.1480
38.2460
38.3430
38.4440
38.5420
38.6400
38.7480
38.8420
38.9460
39370
39.1360
39.2360
39.3450
39.4350
39.5460
39.6460
39.7340
39.8460
39.9380
40320
40.1490
40.2450
40.3450
40.4530
40.5470
40.6390
40.7400
40.8500
40.9400
41370
41.1320
41.2410
41.3500
41.4330
41.5350
41.6400
41.7340
41.8450
41.9450
42410
42.1450
42.2560
42.3350
42.4350
42.5410
42.6430
42.7480
42.8370
42.9330
43310
43.1450
43.2390
43.3450
43.4570
43.5410
43.6360
43.7410
43.8380
43.9460
44520
44.1400
44.2340
44.3350
44.4450
44.5380
44.6470
44.7320
44.8420
44.9420
45360
45.1370
45.2460
45.3380
45.4380
45.5310
45.6390
45.7410
45.8350
45.9460
46390
46.1380
46.2360
46.3390
46.4400
46.5430
46.6360
46.7470
46.8450
46.9390
47280
47.1500
47.2390
47.3450
47.4540
47.5400
47.6380
47.7430
47.8390
47.9490
48370
48.1310
48.2490
48.3420
48.4470
48.5350
48.6370
48.7450
48.8460
48.9280
49320
49.1410
49.2430
49.3380
49.4370
49.5420
49.6320
49.7350
49.8470
49.9330
50440
50.1420
50.2430
50.3440
50.4440
50.5450
50.6450
50.7410
50.8350
50.9380
51380
51.1430
51.2610
51.3450
51.4400
51.5460
51.6430
51.7450
51.8430
51.9380
52390
52.1360
52.2400
52.3380
52.4580
52.5440
52.6340
52.7460
52.8440
52.9380
53360
53.1520
53.2330
53.3490
53.4390
53.5430
53.6350
53.7400
53.8410
53.9410
54390
54.1300
54.2360
54.3350
54.4500
54.5470
54.6460
54.7410
54.8380
54.9440
55290
55.1330
55.2340
55.3450
55.4490
55.5420
55.6350
55.7460
55.8480
55.9410
56430
56.1470
56.2390
56.3440
56.4440
56.5490
56.6430
56.7520
56.8410
56.9440
57370
57.1400
57.2330
57.3480
57.4330
57.5440
57.6350
57.7350
57.8390
57.9340
58310
58.1460
58.2460
58.3450
58.4490
58.5360
58.6420
58.7380
58.8300
58.9380
59420
59.1450
59.2260
59.3370
59.4410
59.5320
59.6330
59.7490
59.8390
59.9430
macOS 26.6.2 · Python 3.13.3 · numpy 2.5.4 · run 2026-10-20
Output · uv run measure-leverage-and-the-cost-of.py
hit rate h = 0.803  ->  leverage 1/(1-h) = 5.1
database reads per 10,000 requests, warm: 1,966 (without the cache: 10,000)
after the clear: peak 1,140 reads/s against 393 reads/s warm = 2.9x (1/(1-h) = 5.1); back under 1.5x warm after 0.6 s
  0.0 s after clear:  1,140 reads/s
  0.1 s after clear:    860 reads/s
  0.2 s after clear:    820 reads/s
  0.3 s after clear:    690 reads/s
  0.4 s after clear:    650 reads/s
  0.5 s after clear:    830 reads/s
  0.6 s after clear:    470 reads/s
  0.7 s after clear:    740 reads/s
  0.8 s after clear:    610 reads/s
  0.9 s after clear:    670 reads/s

What we found

This cache reached h = 0.803, below the 0.95 the chapter assumes: with 5,000 slots for 100,000 Zipf-1.2 keys, about one request in five still misses. That puts the leverage at 5.09×: the database saw 393 reads per second out of 2,000 requests.

Right after the clear the database load jumped to 1,140 reads per second, 2.9× its warm load. That is below the 5.09× that 1/(1 − h) gives, and the spike was short: the load was back under 1.5× warm after 0.6 s. The reason is the skew. The hottest keys come back within the first few dozen requests, so even the first 100 ms already has hits. The spike reaches the full 1/(1 − h) only for the first handful of requests, or when the cache is so large relative to the request rate that refilling it takes minutes.


Read it

Leverage is the number to watch, not the hit rate. Going from h = 0.90 to h = 0.95 looks like a 5-point improvement; it halves the load on the database and doubles the multiple it faces when the cache is lost. A cache that succeeds becomes a dependency nobody drew on the design: the database is now sized for the misses, not for the traffic.

This is why exists. Its purpose is not lower but survivability: after a restart, traffic is ramped back at the rate the database can absorb, not all at once. How long the spike lasts depends on how fast the cache refills, which depends on how concentrated the traffic is and how big the cache is.