ЁЯПл The SchoolтА║ЁЯПОя╕П PerformanceтА║ЁЯЧВя╕П рдзрдбрд╛ 06 тАФ Caching рдЖрдгрд┐ memoization: scoreboard рд▓рдХреНрд╖рд╛рдд рдареЗрд╡рддреЛ
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯЧВя╕П рдзрдбрд╛ 06 тАФ Caching рдЖрдгрд┐ memoization: scoreboard рд▓рдХреНрд╖рд╛рдд рдареЗрд╡рддреЛ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 06 ┬╖ рдорд╛рдЧреЗ: lesson-05-memory ┬╖ рдкреБрдвреЗ: lesson-07-io-batching


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ05, рдЕрдзрд┐рдХ caching: рдЖрдзреАрдЪ рдХрд╛рдврд▓реЗрд▓реЗ рдЙрддреНрддрд░ рдареЗрд╡реВрди рджреНрдпрд╛, рдореНрд╣рдгрдЬреЗ рдкреБрдврдЪреНрдпрд╛ request рд▓рд╛ рддреЗ рдЬрд╡рд│рдЬрд╡рд│ рдлреБрдХрдЯ рдорд┐рд│рддреЗ. Memoization рдореНрд╣рдгрдЬреЗ рдПрдЦрд╛рджреНрдпрд╛ function рдЪреЗ results cache рдХрд░рдгреЗ; рдЬрд╛рдЧрд╛ рдорд░реНрдпрд╛рджрд┐рдд рдЕрд╕рддрд╛рдирд╛ LRU cache рд╕рд░реНрд╡рд╛рдд рдЕрд▓реАрдХрдбреЗ рд╡рд╛рдкрд░рд▓реЗрд▓реА рдЙрддреНрддрд░реЗ рдареЗрд╡рддреЛ; рдЖрдгрд┐ cache рдЙрдкрдпреЛрдЧреА рдЖрд╣реЗ рдХреА рдирд╛рд╣реА рд╣реЗ hit ratio рдард░рд╡рддреЛ. perf/demo.py рдордзрд▓реЗ caching() рдЖрдгрд┐ perf/sim.py рдордзрд▓реЗ fib_plain, fib_memo, LRU, request_stream рдЖрдгрд┐ cached_cost.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдкрд╛рд▓рдХ рд╕рд╛рд░рдЦреЗ results office рдордзреНрдпреЗ рдпреЗрдд рд░рд╛рд╣рддрд╛рдд: "рдХрддрд░рд┐рдирд╛рдЪреА рд╡реЗрд│ рдХрд╛рдп рд╣реЛрддреА?" ЁЯЩЛтАНтЩАя╕П

рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА рдРрд╢реНрд╡рд░реНрдпрд╛ рдорд╛рдЧрдЪреНрдпрд╛ рдЦреЛрд▓реАрдд рдЬрд╛рддреЗ, рдирд┐рдХрд╛рд▓рд╛рдВрдЪреЗ рдореЛрдареЗ рдкреБрд╕реНрддрдХ рдЙрдШрдбрддреЗ, рдкрд╛рди рд╢реЛрдзрддреЗ, рдЖрдгрд┐ рдкрд░рдд рдпреЗрддреЗ. рдпрд╛рдд 50 seconds рдЬрд╛рддрд╛рдд.

рдордЧ рддреА рджрд╛рд░рд╛рдЬрд╡рд│ рдПрдХ рдЫреЛрдЯрд╛ scoreboard рд▓рд╛рд╡рддреЗ. ЁЯУЛ рддреА рд╢реЛрдзрд▓реЗрд▓реЗ рдкреНрд░рддреНрдпреЗрдХ рдЙрддреНрддрд░ board рд╡рд░ рд▓рд┐рд╣рд┐рддреЗ. рдкреБрдврдЪреНрдпрд╛ рд╡реЗрд│реА рдХреЛрдгреА рддреЛрдЪ рдкреНрд░рд╢реНрди рд╡рд┐рдЪрд╛рд░рд▓рд╛, рддрд░ рддреА рдлрдХреНрдд board рд╡рд╛рдЪрддреЗ: 1 second.

Board рд╡рд░ рдлрдХреНрдд 100 рдирд╛рд╡рд╛рдВрд╕рд╛рдареА рдЬрд╛рдЧрд╛ рдЖрд╣реЗ. рддреЛ рднрд░рд▓реНрдпрд╛рд╡рд░, рдЬреНрдпрд╛ рдирд╛рд╡рд╛рдмрджреНрджрд▓ рд╕рд░реНрд╡рд╛рдд рдЬрд╛рд╕реНрдд рдХрд╛рд│ рдХреЛрдгреАрд╣реА рд╡рд┐рдЪрд╛рд░рд▓реЗ рдирд╛рд╣реА рддреЗ рддреА рдкреБрд╕реВрди рдЯрд╛рдХрддреЗ. рдмрд╣реБрддреЗрдХ рдкрд╛рд▓рдХ рддреНрдпрд╛рдЪ рдХрд╛рд╣реА рдЕрдВрддрд┐рдо рдлреЗрд░реАрддрд▓реНрдпрд╛ рдзрд╛рд╡рдкрдЯреВрдВрдмрджреНрджрд▓ рд╡рд┐рдЪрд╛рд░рддрд╛рдд, рдореНрд╣рдгреВрди рдЫреЛрдЯрд╛ board рдЦреВрдк рдкреНрд░рд╢реНрдирд╛рдВрдЪреА рдЙрддреНрддрд░реЗ рджреЗрддреЛ.

рдПрдХ рдзреЛрдХрд╛: рдореЛрдареНрдпрд╛ рдкреБрд╕реНрддрдХрд╛рдд рдПрдЦрд╛рджреА рд╡реЗрд│ рджреБрд░реБрд╕реНрдд рдЭрд╛рд▓реА, рддрд░ рдХреЛрдгреА update рдХрд░реЗрдкрд░реНрдпрдВрдд board рдЕрдЬреВрдирд╣реА рдЬреБрдиреА рд╡реЗрд│ рджрд╛рдЦрд╡рддреЛ. ЁЯХ░я╕П

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    ask["ЁЯЩЛ 2000 lookups<br/>500 runners, a few popular"] --> board{"ЁЯУЛ on the board?<br/>LRU of 100"}
    board -->|"hit 44.9%"| fast["1 ms"]
    board -->|"miss"| book["ЁЯУЪ the back room<br/>50 ms, then write it on the board"]
    fast --> avg["average 28.0 ms<br/>(no cache: 50 ms)"]
    book --> avg
    fib["fib(25): 242,785 calls"] -->|"lru_cache"| memo["26 calls"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/performance/lesson-diagrams.html#l06

тЭУ рдХрд╛рдп

Machines рджрд░рдореНрдпрд╛рдирдЪреЗ caches тАФ рдЕрдиреЗрдХ servers рдкреБрдврдЪреЗ CDNs рдЖрдгрд┐ shared caches тАФ рд╣реЗ Scaling school рдордзреНрдпреЗ рдЖрд╣реЗрдд.

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рд╕рд░реНрд╡рд╛рдд рд╕реНрд╡рд╕реНрдд рдХрд╛рдо рдореНрд╣рдгрдЬреЗ рдЬреЗ рддреБрдореНрд╣реА рд╡рдЧрд│рддрд╛ рддреЗ. рд╕рд╛рдзреЗ recursive fib(25) рддреЗрдЪ рдЙрдк-рдкреНрд░рд╢реНрди рд▓рд╛рдЦреЛ рд╡реЗрд│рд╛ рдкреБрдиреНрд╣рд╛ рдХрд░рддреЗ; cache рдкреНрд░рддреНрдпреЗрдХрд╛рд▓рд╛ рдПрдХрд╛рдЪ рдЧрдгрдиреЗрдд рдмрджрд▓рддреЛ. рдкрдг cache рдореНрд╣рдгрдЬреЗ memory рд╕реБрджреНрдзрд╛ рдЖрд╣реЗ (рдзрдбрд╛ 05), рдЪреБрдХреАрдЪрд╛ data рджреЗрдгреНрдпрд╛рдЪрд╛ рдПрдХ рдирд╡рд╛ рдорд╛рд░реНрдЧ, рдЖрдгрд┐ restart рдирдВрддрд░ warm up рдХрд░рд╛рд╡реА рд▓рд╛рдЧрдгрд╛рд░реА рдЖрдгрдЦреА рдПрдХ рдЧреЛрд╖реНрдЯ. рдЖрдзреА рдЖрдгрд┐ рдирдВрддрд░ hit ratio рдореЛрдЬрд╛; 5% hit ratio рдЕрд╕рд▓реЗрд▓рд╛ cache рдмрд╣реБрддрд╛рдВрд╢ рдЦрд░реНрдЪрдЪ рдЕрд╕рддреЛ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

perf/sim.py рдордзрд▓реЗ fib_plain(n) рдЖрдкрд▓реНрдпрд╛ calls CALLS["fib"] рдордзреНрдпреЗ рдореЛрдЬрддреЗ; fib_memo(n) рддреЗрдЪ recursion functools.lru_cache рдордзреНрдпреЗ рдЧреБрдВрдбрд╛рд│рддреЗ рдЖрдгрд┐ рдЙрддреНрддрд░ рдЖрдгрд┐ рдЦрд░реЗ cache_info() рдкрд░рдд рджреЗрддреЗ. LRU(cap) рд╣рд╛ OrderedDict рд╡рд░ рдмрд╛рдВрдзрд▓реЗрд▓рд╛ рдЫреЛрдЯрд╛ least-recently-used cache рдЖрд╣реЗ, рдЬреЛ hits рдЖрдгрд┐ misses рдореЛрдЬрддреЛ. request_stream() 500 keys рд╡рд░ рдЬреЛрд░рджрд╛рд░ skew рд╕рд╣ 2000 seeded lookups рдмрдирд╡рддреЗ, рдЖрдгрд┐ cached_cost(cap, stream, hit_ms, miss_ms) hit ratio рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ request рдЪрд╛ simulated рд╕рд░рд╛рд╕рд░реА рдЦрд░реНрдЪ рдкрд░рдд рджреЗрддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 perf/demo.py caching
python3 - <<'EOF'
import sys; sys.path.insert(0, "perf"); import sim
for n in (10, 20, 25, 30):
    sim.CALLS["fib"] = 0; sim.fib_plain(n); plain = sim.CALLS["fib"]
    sim.CALLS["fib"] = 0; sim.fib_memo(n); memo = sim.CALLS["fib"]
    print(f"fib({n:>2}): plain {plain:>9,} calls ┬╖ memoized {memo:>3}")
s = sim.request_stream()
for miss_ms in (5, 50, 500):
    r, cost = sim.cached_cost(100, s, miss_ms=miss_ms)
    print(f"cache of 100, a miss costs {miss_ms:>3} ms тЖТ hit ratio {r:.1%} ┬╖ average {cost:6.1f} ms")
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

caching рд╣реЗ рдЫрд╛рдкрддреЗ:

тФАтФА fib(25) = 75025: plain recursion does the work 242,785 times ┬╖ memoized (lru_cache) 26 times (23 cache hits)
тФАтФА parents look up results: 2000 requests over 500 runners, a few asked for far more often ┬╖ a hit costs 1 ms, a miss 50 ms
   LRU cache of  10 тЖТ hit ratio 15.4% ┬╖ average 42.5 ms per request
   LRU cache of  50 тЖТ hit ratio 33.0% ┬╖ average 33.9 ms per request
   LRU cache of 100 тЖТ hit ratio 44.9% ┬╖ average 28.0 ms per request
   LRU cache of 250 тЖТ hit ratio 67.7% ┬╖ average 16.8 ms per request

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

fib(10): plain       177 calls ┬╖ memoized  11
fib(20): plain    21,891 calls ┬╖ memoized  21
fib(25): plain   242,785 calls ┬╖ memoized  26
fib(30): plain 2,692,537 calls ┬╖ memoized  31
cache of 100, a miss costs   5 ms тЖТ hit ratio 44.9% ┬╖ average    3.2 ms
cache of 100, a miss costs  50 ms тЖТ hit ratio 44.9% ┬╖ average   28.0 ms
cache of 100, a miss costs 500 ms тЖТ hit ratio 44.9% ┬╖ average  275.9 ms

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

Memoization рдиреЗ рдШрд╛рддрд╛рдВрдХреА (exponential) рдХрд╛рдорд╛рдЪреЗ рд░реЗрд╖реАрдп (linear) рдХрд╛рдорд╛рдд рд░реВрдкрд╛рдВрддрд░ рдХреЗрд▓реЗ: fib(30) 2,692,537 calls рд╡рд░реВрди 31 рд╡рд░ рдЖрд▓реЗ. Lookup cache рд╕рд╛рдареА, 10 slots рдиреАрдЪ 15.4% requests рдкрдХрдбрд▓реНрдпрд╛, рдХрд╛рд░рдг traffic skewed рд╣реЛрддреЗ, рдкрдг cache рджреБрдкреНрдкрдЯ рдХреЗрд▓реНрдпрд╛рдиреЗ hit ratio рдХрдзреАрдЪ рджреБрдкреНрдкрдЯ рдЭрд╛рд▓рд╛ рдирд╛рд╣реА. рдЖрдгрд┐ 500 ms рдЪреНрдпрд╛ miss рд╕рд╣, 44.9% hit ratio рдЕрд╕реВрдирд╣реА рд╕рд░рд╛рд╕рд░реА 275.9 ms рдЙрд░рддреЗ: рдЬреЗрд╡реНрд╣рд╛ misses рдорд╣рд╛рдЧ рдЕрд╕рддрд╛рдд, рддреЗрд╡реНрд╣рд╛ miss path рд╕реБрджреНрдзрд╛ рдЬрд▓рдж рдЕрд╕рд╛рдпрд▓рд╛ рд╣рд╡рд╛.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ Redis рд╕рд╣ cache-aside, TTL рд╕рд╣:

import json, redis
r = redis.Redis(host="cache.school.internal")
def best_lap(runner_id):
    key = f"best_lap:{runner_id}"
    hit = r.get(key)
    if hit is not None:
        return json.loads(hit)
    value = db_best_lap(runner_id)                  # the slow source
    r.set(key, json.dumps(value), ex=60)            # expire after 60 s
    return value

Redis рддреБрдордЪреНрдпрд╛рд╕рд╛рдареА рдареЗрд╡рдд рдЕрд╕рд▓реЗрд▓рд╛ hit ratio рддрдкрд╛рд╕рд╛:

redis-cli INFO stats | grep -E 'keyspace_hits|keyspace_misses'
redis-cli CONFIG SET maxmemory-policy allkeys-lru   # evict least recently used keys when full

Python process рдордзреНрдпреЗ, рдорд░реНрдпрд╛рджрд┐рдд memo:

from functools import lru_cache
@lru_cache(maxsize=1024)
def house_of(bib: int) -> str: ...
print(house_of.cache_info())      # hits, misses, maxsize, currsize

ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдЖрд╣реЗ: рдкреНрд░рддреНрдпреЗрдХ cache рд╕рд╛рдареА, рддреНрдпрд╛рдЪрд╛ hit ratio рдЖрдгрд┐ рддреНрдпрд╛рдЪреА miss latency chart рдХрд░рд╛, рдЖрдгрд┐ рддреНрдпрд╛рдЪрд╛ data рдХрд┐рддреА рд╢рд┐рд│рд╛ рдЕрд╕реВ рд╢рдХрддреЛ рддреЗ рд▓рд┐рд╣реВрди рдареЗрд╡рд╛. рдХреЛрдгреАрд╣реА рди рдореЛрдЬрд▓реЗрд▓рд╛ cache рдореНрд╣рдгрдЬреЗ рдлрдХреНрдд рдЕрдВрджрд╛рдЬ.

тПня╕П рдкреБрдвреЗ

Misses "рдорд╛рдЧрдЪреНрдпрд╛ рдЦреЛрд▓реАрдд" рдЧреЗрд▓реЗ. рддрд┐рдерд▓реА рдкреНрд░рддреНрдпреЗрдХ рдлреЗрд░реА рдореНрд╣рдгрдЬреЗ рд╡реЗрд│ рдЦрд╛рдгрд╛рд░реЗ рдПрдХ рд╣рд╕реНрддрд╛рдВрддрд░ (hand-off). рдкреБрдвреЗ: I/O тАФ N+1 queries, batching рдЖрдгрд┐ buffering.

git checkout lesson-07-io-batching

ЁЯЧВя╕П Lesson 06 тАФ Caching & memoization: the scoreboard remembers

ЁЯУН You are here: Lesson 06 of 12 ┬╖ Previous: lesson-05-memory ┬╖ Next: lesson-07-io-batching


ЁЯУж What's in this branch

Lessons 01тАУ05, plus caching: keep an answer you already worked out, so the next request gets it for almost nothing. Memoization is caching the results of a function; an LRU cache keeps the most recently used answers when space is limited; and the hit ratio decides whether a cache helps at all. caching() in perf/demo.py and fib_plain, fib_memo, LRU, request_stream and cached_cost in perf/sim.py.

ЁЯзТ Explain like I'm 5

Parents keep coming to the results office: "What was Katrina's time?" ЁЯЩЛтАНтЩАя╕П

Each time, Aishwarya walks to the back room, opens the big results book, finds the page, and walks back. That takes 50 seconds.

Then she puts up a small scoreboard by the door. ЁЯУЛ Every answer she looks up goes on the board. Next time someone asks the same question, she just reads the board: 1 second.

The board only has room for 100 names. When it is full, she rubs out the name nobody asked about for the longest time. Most parents ask about the same few finalists, so a small board answers a lot of questions.

One danger: if a time is corrected in the big book, the board still shows the old time until someone updates it. ЁЯХ░я╕П

ЁЯЧ║я╕П Diagram

flowchart LR
    ask["ЁЯЩЛ 2000 lookups<br/>500 runners, a few popular"] --> board{"ЁЯУЛ on the board?<br/>LRU of 100"}
    board -->|"hit 44.9%"| fast["1 ms"]
    board -->|"miss"| book["ЁЯУЪ the back room<br/>50 ms, then write it on the board"]
    fast --> avg["average 28.0 ms<br/>(no cache: 50 ms)"]
    book --> avg
    fib["fib(25): 242,785 calls"] -->|"lru_cache"| memo["26 calls"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/performance/lesson-diagrams.html#l06

тЭУ What

Caches between machines тАФ CDNs and shared caches in front of many servers тАФ are covered in the Scaling school.

ЁЯдФ Why

Because the cheapest work is the work you skip. Plain recursive fib(25) repeats the same sub-questions hundreds of thousands of times; a cache makes each one a single computation. But a cache is also memory (lesson 05), a new way to serve wrong data, and one more thing to warm up after a restart. Measure the hit ratio before and after; a cache with a 5% hit ratio is mostly cost.

ЁЯФз How (in this repo)

fib_plain(n) in perf/sim.py counts its calls in CALLS["fib"]; fib_memo(n) wraps the same recursion in functools.lru_cache and returns the answer and the real cache_info(). LRU(cap) is a small least-recently-used cache built on OrderedDict, counting hits and misses. request_stream() makes 2000 seeded lookups over 500 keys with a strong skew, and cached_cost(cap, stream, hit_ms, miss_ms) returns the hit ratio and the simulated average cost per request.

ЁЯзк Try it

python3 perf/demo.py caching
python3 - <<'EOF'
import sys; sys.path.insert(0, "perf"); import sim
for n in (10, 20, 25, 30):
    sim.CALLS["fib"] = 0; sim.fib_plain(n); plain = sim.CALLS["fib"]
    sim.CALLS["fib"] = 0; sim.fib_memo(n); memo = sim.CALLS["fib"]
    print(f"fib({n:>2}): plain {plain:>9,} calls ┬╖ memoized {memo:>3}")
s = sim.request_stream()
for miss_ms in (5, 50, 500):
    r, cost = sim.cached_cost(100, s, miss_ms=miss_ms)
    print(f"cache of 100, a miss costs {miss_ms:>3} ms тЖТ hit ratio {r:.1%} ┬╖ average {cost:6.1f} ms")
EOF

тЬЕ Verify тАФ what you should see

caching prints:

тФАтФА fib(25) = 75025: plain recursion does the work 242,785 times ┬╖ memoized (lru_cache) 26 times (23 cache hits)
тФАтФА parents look up results: 2000 requests over 500 runners, a few asked for far more often ┬╖ a hit costs 1 ms, a miss 50 ms
   LRU cache of  10 тЖТ hit ratio 15.4% ┬╖ average 42.5 ms per request
   LRU cache of  50 тЖТ hit ratio 33.0% ┬╖ average 33.9 ms per request
   LRU cache of 100 тЖТ hit ratio 44.9% ┬╖ average 28.0 ms per request
   LRU cache of 250 тЖТ hit ratio 67.7% ┬╖ average 16.8 ms per request

Your snippet prints:

fib(10): plain       177 calls ┬╖ memoized  11
fib(20): plain    21,891 calls ┬╖ memoized  21
fib(25): plain   242,785 calls ┬╖ memoized  26
fib(30): plain 2,692,537 calls ┬╖ memoized  31
cache of 100, a miss costs   5 ms тЖТ hit ratio 44.9% ┬╖ average    3.2 ms
cache of 100, a miss costs  50 ms тЖТ hit ratio 44.9% ┬╖ average   28.0 ms
cache of 100, a miss costs 500 ms тЖТ hit ratio 44.9% ┬╖ average  275.9 ms

ЁЯПБ What you just proved

Memoization turned exponential work into linear: fib(30) went from 2,692,537 calls to 31. For the lookup cache, 10 slots already caught 15.4% of requests, because the traffic was skewed, but doubling the cache never doubled the hit ratio. And with a 500 ms miss, even a 44.9% hit ratio leaves an average of 275.9 ms: when misses are expensive, the miss path still needs to be fast.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ cache-aside with Redis, with a TTL:

import json, redis
r = redis.Redis(host="cache.school.internal")
def best_lap(runner_id):
    key = f"best_lap:{runner_id}"
    hit = r.get(key)
    if hit is not None:
        return json.loads(hit)
    value = db_best_lap(runner_id)                  # the slow source
    r.set(key, json.dumps(value), ex=60)            # expire after 60 s
    return value

Check the hit ratio Redis keeps for you:

redis-cli INFO stats | grep -E 'keyspace_hits|keyspace_misses'
redis-cli CONFIG SET maxmemory-policy allkeys-lru   # evict least recently used keys when full

In a Python process, a bounded memo:

from functools import lru_cache
@lru_cache(maxsize=1024)
def house_of(bib: int) -> str: ...
print(house_of.cache_info())      # hits, misses, maxsize, currsize

ЁЯПн Why this matters in production: for every cache, chart its hit ratio and its miss latency, and write down how stale its data may be. A cache nobody measures is a guess.

тПня╕П Next

The misses went to "the back room". Every trip there is a hand-off that costs time. Next: I/O тАФ N+1 queries, batching and buffering.

git checkout lesson-07-io-batching
тЖР PreviousmemoryNext тЖТio batching

This page is the lesson's README from the lesson-06-caching branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.