🟣 Advanced best practices
You already understand caching and what breaks it. Now comes the day-to-day rule of thumb: when caching solves the problem and when compact resolves how to design the prefix so that never invalidate unintentionally, and a kit of ready-to-use prompts to run in Claude Code today.
Legend: cache moves the vertical axis down (price per token, ÷10) but doesn’t change the width; compact runs along the horizontal axis (number of tokens, ÷20). They’re different axes — you want both.
What you'll master by the end of this track
The cache is no longer a mystery to you. What’s left is make good decisions: the cache makes cheaper the price per token, but it doesn’t reduce how many tokens you load — and the context limit (1M) is about quantity. This track teaches you to read both axes and build the prefix for it never invalidate by accident, and provides ready-to-use prompts to audit and fix the cache directly in Claude Code.
Caching vs. compaction
Two different axes. When cached context still costs too much, and when 1 cold compaction write pays for itself.
Cache-friendly architecture
Frozen system, deterministic tools, a fork that reuses the parent’s prefix, a silent invalidator checklist, and pre-warming.
Ready-to-use prompts
6 copyable prompts: audit cache, order tools, decide when to compact, diagnose cache_read=0, review the system and plan breakpoints.
Trail map
⚖️ Cache vs. Compaction
Two axes, one ceiling
🏛️ Architecture and audit
Freeze the prefix
📋 Ready-to-use prompts
Copy, paste, run
Detailed content
⚖️ Cache vs. Compaction
Caching optimizes the price per token; compaction optimizes how many tokens you carry. Different axes—and only compaction solves the context limit.
Caching optimizes the price per token of the context. Compact (summarize the history into a shorter version; in Claude Code, /compact) optimizes how many tokens you load.
Without separating the axes, you think caching “solves everything” and ignore the context limit—or compact too early and lose the warm cache.
They don’t compete on the same axis; you want both. The decision is when to apply each one.
Caching lowers the cost per token, but 400k tokens read at 0.1× still equal ~40k input tokens full per turn, every turn.
The discount is misleading: a huge context that’s cheap per token is still expensive overall.
0.1× of a lot is still a lot; volume matters even with a high hit rate.
The cache makes 900k tokens cheap, but they still take up 900k of 1M. At some point, you can’t continue without compacting or /clear.
The limit is about quantity; only compacting/clearing frees up space.
Cheap ≠ small; the limit is on quantity, not price.
Without compaction: 400k @0.1× ≈ 40k-equiv per turn, forever. Compacting to 20k: 1 cold write of 20k @1.25× ≈ 25k (once), then 20k @0.1× ≈ 2k-equiv per turn.
Payback in ~1 turn; each following turn is ~20× cheaper.
Replaces one-time cost with much cheaper turns.
A bloated context spreads the model’s attention (“context rot”) and, even when read from cache, 400k takes longer to reach the first token (higher TTFT) than 20k.
Compaction improves accuracy and speed, not just cost.
Fewer tokens = more focus + faster response.
Short conversation → DO NOT compact (you only lose the warm cache without any benefit). Large context, near the limit, or quality declining → compact.
The intuition that “compacting always saves money” is false for short conversations.
Replaces 1 cold write with lighter turns — only worthwhile when enough turns remain.
🏛️ Cache-friendly architecture and audit
Design the prefix so it never gets invalidated by accident: frozen system, deterministic tools, a fork that copies the parent’s prefix, a checklist, and prewarming.
Don’t interpolate the date/time, mode, or user’s name into the system prompt — it comes before the prefix and invalidates everything. Inject dynamic context later, in the messages.
A variable in the system invalidates the entire tools + system cache every turn.
Stable system = stable prefix; dynamic content goes at the end.
Serialize the list of tools always in the same order (sort by name). An unsorted set/dict = variable order = miss.
Tools stay at position 0; an unstable order silently invalidates everything.
Same order = same bytes = hit.
Caching is model-specific. Switching models invalidates everything. Need a cheaper model for a subtask? Use a subagent while keeping the main loop on a single model.
Switching models “just for this one” restarts the entire prefix.
One model per loop; delegate variation to subagents.
A side operation (summary, subagent) that rebuilds system/tools/model differently loses the parent's cache. Copy system/tools/model verbatim and append the specific content at the end.
A well-made fork inherits the warm cache and only writes the new block.
Identical to the parent's prefix + a turn-specific suffix at the end.
Scan what feeds the prefix: datetime.now(), uuid, json.dumps without sort_keys, sets, per-user tools, conditional sections in the system.
These are the culprits behind cache_read_input_tokens always 0.
Byte-by-byte deterministic prefix.
Request max_tokens: 0 when the app starts, it writes the prefix cache and returns immediately. TTL 1h (write ~2×) for traffic with gaps > 5 min.
Only worthwhile if the first latency is noticeable, the prefix is large, and there’s a moment before traffic arrives.
Warm the cache before the first user; use a 1-hour TTL for long pauses.
📋 Ready-to-use prompts (copy-run)
Six real, ready-to-paste prompts: each with an objective, a copyable block (with the parts you replace), and how to verify the result in Claude Code.
Prompt that asks Claude Code to scan the code that builds the prompt and list every silent invalidator + the fix.
Turns “my cache isn’t hitting” into an actionable checklist.
Objective + copy-ready block + how to verify.
Prompt that asks to sort the serialization of tools by name and confirm the order is stable across requests.
Eliminates the #1 source of silent misses at position 0.
Stable serialization = identical prefix.
Prompt that estimates the per-turn cost of loading the current context versus the one-time cost of compacting it, and makes a recommendation.
Takes the decision out of guesswork and puts numbers on it.
Cost per turn vs. one-time cold write.
Prompt that diffs the prefix bytes between two identical requests and points out the byte that changes.
cache_read_input_tokens always 0 = there’s an invalidator; the diff will find it.
Two requests, one diff, the guilty byte.
Prompt that removes dynamic interpolation from the system prompt and moves it to the end of the messages.
Freeze the system without losing dynamic context.
Stable at the beginning, dynamic at the end.
Prompt that suggests where to put cache_control (end of the stable system prompt; end of the shared section in a variable prefix + suffix) and a warning about the 20-block lookback.
A few breakpoints in the right places go further.
Max. 4 breakpoints; watch out for the 20-block lookback.