🧊 System prompt congelado
O prompt de sistema (el texto de instrucciones antes de la conversación) queda justo al principio del prefijo. Si interpolas fecha/hora, modo o nombre del usuario ahí, cada solicitud tiene bytes diferentes al comienzo — y invalida todo. La regla es: system congelado; el contexto dinámico va después, en las messages.
Leyenda: lo dinámico no desaparece — cambia de lugar. Sale de adelante (system) y va al final (messages), donde no rompe el prefijo.
System sin variables
Va al final de los messages
System está casi en la posición 0
Basta para invalidar
🔤 Tools deterministas
La lista de tools se muestra en la posición 0 — antes que todo. Si la armas de una set/dict sin ordenar, el orden varía entre solicitudes y la caché nunca coincide. Serializa siempre en el mismo orden: ordena por nombre.
✗ Orden variable
tools = [t for t in registry] # registry é um set/dict # ordem muda entre requests → miss
✓ Orden estable
tools = sorted(registry, key=lambda t: t["name"]) # mesma ordem sempre → prefixo idêntico # cache_read para de ser 0
Objetivo
Hacer que Claude Code señale qué, en tu system prompt y en el montaje de las tools, está impidiendo el caché, y reescribir para maximizar el hit.
Prompt (pégalo en 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.
Cómo verificar
Haz dos solicitudes iguales después del cambio y revisa el usage: cache_read_input_tokens debe dejar de ser 0 a partir de la 2.ª. Si aún aparece 0, quedó un invalidator en el prefijo (consulta el tema 5).
Tools van antes que todo
Ordena por nombre
El orden varía = miss
Mismos bytes = hit
🚫 No cambiar de modelo a mitad de camino
La caché es por modelo. Cambiar de modelo a mitad de camino lo invalida todo: el prefijo almacenado en caché de un modelo no sirve para el otro. ¿Necesitas un modelo más barato para una subtarea? Usa un subagente, manteniendo el loop principal en un solo modelo.
Leyenda: el loop morado queda en un solo modelo; el subagente cian usa otro modelo sin tocar la caché del padre.
Por qué lo resuelve un subagente
El subagente tiene su propio prefijo y su propia caché. Ejecuta la subtarea con el modelo más económico y devuelve el resultado como texto; el bucle principal sigue usando el mismo modelo, con la caché intacta.
La caché no se comparte entre modelos
Invalida todo
Modelo barato aparte
Mantiene la caché caliente
🌿 Fork reutiliza el prefijo del padre
Una operación lateral — un resumen, un subagente — que vuelve a armar system/tools/model diferentes pierde la caché del padre. Para reutilizar el prefijo caliente, el fork necesita copiar system/tools/model verbatim y solo anexar lo específico al final.
Leyenda: el fork cian hereda el prefijo del padre (solo guarda el sufijo); el fork rojo lo reconstruye y vuelve a cobrar todo.
✓ Fork que reutiliza
- ✓Copia
systembyte a byte - ✓Mantiene la misma lista de
toolsy el mismomodel - ✓Anexa la instrucción específica solo al final
✗ Fork que pierde
- ✗Escribe un system nuevo «conciso» para el resumen
- ✗Pasa otro conjunto de tools u otro modelo
- ✗Resultado: prefijo diferente, caché desde cero
Copia system/tools/model
Anexa lo específico
Reutiliza el caché del padre
= pagar desde cero
🔎 Lista de verificación de invalidadores silenciosos
Uno invalidator silencioso no da error — solo hace cache_read_input_tokens quedar siempre en 0. Antes de culpar al modelo, revisa todo lo que alimenta el prefijo y busca a estos sospechosos.
Leyenda: cualquier fuente de variación en lo que se convierte tools/system cambia el prefijo y derriba la caché en silencio.
Los 6 sospechosos de siempre
- ☐
datetime.now()/Date.now()en system - ☐
uuido un id aleatorio en el prefijo - ☐
json.dumpssinsort_keys=True - ☐Iterar un
set(orden no garantizado) - ☐
toolsconfigurados por usuario/modo - ☐Sección condicional en el system (
if flag: system += …)
Síntoma → causa
cache_read_input_tokens siempre 0 en requests repetidos = hay un invalidador. Compara con diff los bytes del prefijo entre dos requests iguales y el byte que cambia es el culpable (el tema 4 del módulo 3.3 tiene el prompt listo para eso).
No da error
El único síntoma
Revisa el prefijo
Encuentra el byte responsable
🔥 Precalentamiento y TTL 1h
Una solicitud con max_tokens: 0 al iniciar la app guarda el caché del prefijo y regresa de inmediato — elimina la latencia del primer request real. Solo sirve si se cumplen tres condiciones; y, para el tráfico con pausas largas, existe también el TTL de 1h (escritura ~2×).
Leyenda: el precalentamiento paga el write antes del tráfico, para que el primer usuario no note la latencia de la escritura.
✓ Precargar vale cuando
- ✓La 1.ª latencia es visible para el usuario
- ✓El prefijo es grande (vale la pena guardarlo)
- ✓Hay un momento antes del tráfico (boot/deploy)
TTL de 1h
- •Write más caro (~2× en lugar de ~1,25×)
- •Aplica al tráfico con gaps > 5 min
- •Mantiene caliente el caché durante pausas largas
Registra sin generar una respuesta
Boot/deploy
Brechas > 5 min
Write de 1h
Resumen del módulo
max_tokens: 0 en el boot; 1 h para gaps largos.Siguiente módulo
3.3 — Prompts listos (copiar y ejecutar): el kit de 6 prompts para auditar y corregir la caché en Claude Code.