PTENES
Skip to content
MODULE 3.2

🏛️ Cache-friendly architecture and audit

The best cache is the one that never invalidates by accident. Here you design the prefix — frozen system, deterministic tools, one model per loop, forks that copy the parent — and get a checklist and an audit prompt to run in Claude Code.

6
Topics
~45
Minutes
Advanced
Level
Practice
Type
1

🧊 Frozen system prompt

O system prompt (the instruction text before the conversation) sits right at the front of the prefix. If you interpolate date/time, mode, or username there, each request has different bytes at the beginning — and invalidates everything. The rule is: frozen system; dynamic context goes after it, in the messages.

✗ interpolated system system + "today is {data}, user {nome}" changes every turn messages → cache invalidates every turn ✓ frozen system fixed system (no variables) 🧊 identical byte for byte messages + "today is {data}, user {nome}" → stable prefix, warm cache

Legend: the dynamic part doesn't disappear — it changes position. Moves out of the way (system) and to the end (messages), where it doesn't break the prefix.

Freeze

System without variables

Dynamic

Goes to the end of the messages

Front

System is almost position 0

1 byte

Enough to invalidate it

2

🔤 Deterministic tools

The list of tools is rendered in the position 0 — before anything else. If you build it from one set/dict without sorting, the order varies across requests and the cache never hits. Always serialize in the same order: sort by name.

✗ Variable order

tools = [t for t in registry]
# registry é um set/dict
# ordem muda entre requests → miss

✓ Stable order

tools = sorted(registry, key=lambda t: t["name"])
# mesma ordem sempre → prefixo idêntico
# cache_read para de ser 0
Ready-to-use prompt — cache-friendly review (system + tools)

Objective

Have Claude Code point out what, in your system prompt and in the assembly of the tools, is preventing the cache from working—and rewriting to maximize the hit rate.

Prompt (paste into Claude Code)

Revise meu system prompt e a montagem de `tools` em <caminho/do/arquivo>.
Aponte o que está impedindo o prompt caching:
1) interpolação dinâmica no system (data/hora, nome do usuário, modo,
   uuid, qualquer coisa que muda entre requests);
2) ordem não-determinística das tools (montadas de set/dict sem sort).
Depois reescreva: (a) congele o system e mova o dinâmico pro fim das
messages; (b) serialize as tools em ordem estável (ordene por nome).
Mostre o diff e explique por que cada mudança preserva o prefixo.

How to verify

Run two identical requests after the change and check the usage: cache_read_input_tokens must stop being 0 starting with the 2nd. If it’s still 0, an invalidator remains in the prefix (see topic 5).

Position 0

Tools come before everything

sorted()

Sort by name

set/dict

Order varies = miss

Stable

Same bytes = hit

3

🚫 Don't switch models midstream

Caching is per model. Switching models midstream invalidates everything — one model's cached prefix doesn't work for another. Need a cheaper model for a subtask? Use a subagent, keeping the main loop on a single model.

Main loop — one model (warm cache) model A model A model A delegate a subtask subagent · low-cost model its own cache, doesn’t touch the parent’s switch to model B in the loop → full reset

Legend: the loop purple stays in one model; the subagent cyan uses another model without touching the parent’s cache.

Why a subagent helps

The subagent has its own prefix and its own cache. It runs the subtask on the cheaper model and returns the result as text—the main loop continues on the same model, with the cache intact.

By model

Caching doesn’t work across models

Replace = reset

Invalidates everything

Subagent

Cheap model on the side

One per loop

Keeps the cache warm

4

🌿 Fork reuses the parent prefix

A side operation — a summary, a subagent — that rebuilds system/tools/model different loses the parent's cache. To reuse the warm prefix, the fork needs to copy system/tools/model verbatim and just append the specific part at the end.

PARENT — hot prefix tools → system → model already cached ✓ copy verbatim + append at the end tools → system → model (identical) + task reuses the parent’s cache · only writes the new block ✗ rebuilds different system/tools/model pays for the prefix from scratch

Legend: the fork cyan inherits the parent’s prefix (only writes the suffix); the fork red rebuilds it and pays for everything again.

✓ Fork that reuses

  • ✓Copy system byte by byte
  • ✓Keeps the same list of tools and the same model
  • ✓Append the specific instruction only at the end

✗ Fork that loses

  • ✗Write a new "lean" system prompt for the summary
  • ✗Passes another set of tools or another model
  • ✗Result: different prefix, cache starts from scratch
Verbatim

Copy system/tools/model

At the end

Append the specific part

Inherits

Reuses the parent's cache

Rebuild

= pay from scratch

5

🔎 Silent invalidator checklist

One silent invalidator doesn’t error—it just cache_read_input_tokens always stay 0. Before blaming the model, scan everything that feeds the prefix and look for these suspects.

Scan what goes into the prefix tools system messages looking for variation clock · uuid · sort · set

Legend: any source of variation in what becomes tools/system changes the prefix — and silently breaks the cache.

The 6 usual suspects

  • ☐datetime.now() / Date.now() in the system
  • ☐uuid or a random ID in the prefix
  • ☐json.dumps without sort_keys=True
  • ☐Iterate a set (order not guaranteed)
  • ☐tools assembled by user/mode
  • ☐Conditional section in system (if flag: system += …)

Symptom → cause

cache_read_input_tokens always 0 on repeated requests = there’s an invalidator. Diff the prefix bytes between two identical requests, and the byte that changes is the culprit (topic 4 of module 3.3 has the ready-to-use prompt for this).

Silent

No error

read = 0

The single symptom

6 suspects

Scan the prefix

Diff

Find the byte at fault

6

🔥 Warm-up and TTL 1h

A request with max_tokens: 0 when the app starts writes the prefix cache and returns immediately — removes latency from the first real request. This only applies if three things are true; and for traffic with long pauses, there's also the TTL of 1h (write ~2×).

time app starts max_tokens: 0 writes the cache · returns immediately 1st user prefix already warm → fast response

Legend: prewarming pays for the write before of traffic, so the first user doesn’t feel the write latency.

✓ Prewarming is worthwhile when

  • ✓The 1st latency is visible to the user
  • ✓The prefix is large (the write is worthwhile)
  • ✓There is a moment before traffic (boot/deploy)

TTL of 1h

  • •More expensive write (~2× instead of ~1.25×)
  • •Applies to traffic with gaps > 5 min
  • •Keeps the cache warm through long pauses
max_tokens: 0

Records without generating a response

Before traffic

Boot/deploy

TTL 1h

Gaps > 5 min

~2×

1 h write

Module summary

✓
Frozen system — no date/time/name; dynamic content goes to the end of the messages.
✓
Deterministic tools — sort by name; set/dict without sorting = cache miss.
✓
One model per loop — switching models resets it; use a subagent for a cheap subtask.
✓
Fork copies the parent — system/tools/model verbatim + specific content at the end.
✓
Invalidator checklist — clock, uuid, sort, set, per-user tools, conditional in system.
✓
Pre-warming and 1h TTL — max_tokens: 0 at boot; 1h for long gaps.

Next module

3.3 — Ready-to-use prompts (copy-run): the kit of 6 prompts to audit and fix caching in Claude Code.