Fallos de caché de prompts en Claude Code: por qué un 86 % de aciertos oculta el coste
Un modelo con precios de una sesión en un monorepo de iOS muestra siete fallos de caché de prompts que pesan más que 33 aciertos, y cómo estabilizar herramientas, skills y CLAUDE.md como claves de caché.
Una tasa de aciertos del 86 % suena sana. Sin embargo, en una tarde modelada de trabajo con Claude Code, siete fallos de caché costaron más que los otros 33 turnos juntos. El panorama cambia en cuanto le pones precio a cada fallo y lo rastreas hasta su causa.
La versión 2.1.260 de Claude Code añadió una línea nueva a /cost: una estimación de por qué ocurrió tu último fallo de caché de prompts. Los candidatos son definiciones de herramientas modificadas, un prompt de sistema modificado o una pausa más larga que el TTL. Luego la 2.1.261 trajo /skill-doctor, que informa de skills cargadas pero nunca usadas y de lo que cuestan en contexto. Esa misma versión corrigió un bug por el que los compañeros de un equipo de agentes repetían en el segundo turno sus anuncios de herramientas del primero, alterando el prefijo de la petición y fallando la caché.
Leídas juntas, esas entradas dicen que el proveedor ha empezado a tratar el montaje del contexto como ingeniería de claves de caché, y la mayoría de los usuarios no. Una caché de prompts es una caché de prefijos. Todo lo que va delante de la conversación, en el orden en que lo pusiste, es la clave. Para ver lo que eso cuesta en un día normal, las cifras de abajo salen del modelo de una sola sesión.
Cómo construye la caché la clave de cada petición
La documentación de caché de Anthropic detalla la estructura. Las peticiones se organizan en capas: herramientas, luego system, luego mensajes, y cualquier cambio en una capa borra esa capa y todas las siguientes. Renombrar una herramienta o tocar su esquema tira la caché entera; editar el prompt de sistema conserva la entrada de herramientas y pierde el resto. Con cada petición, la API calcula el hash del prefijo hasta tu breakpoint y, si nada coincide, retrocede bloque a bloque mirando como máximo 20 posiciones. Puedes fijar hasta cuatro breakpoints; en este escenario van tras el array de herramientas, tras el prompt de sistema y sobre el último mensaje.
Por defecto una entrada vive cinco minutos, y cada lectura la renueva gratis. Escribir una entrada de una hora cuesta el doble del precio base de entrada. Claude Fable 5.1 cobra 10 $ por millón de tokens de entrada, 12,50 $ por millón por escribir una entrada de cinco minutos, 20 $ por una de una hora y 0,25 $ por millón por leer. Leer de la caché cuesta, por tanto, el 2,5 % de enviar esos mismos tokens sin caché, y una escritura nueva de cinco minutos cuesta 50 veces una lectura. Así que un fallo no es una pequeña ralentización: todo lo que se cayó de la caché pasa de golpe a facturarse 50 veces más caro.
Una tarde, siete sucesos corrientes
El paquete Swift ContextCacheKit ordena una petición tal como la API calcula su hash, simula una caché con las reglas documentadas de TTL y retroceso y pone precio a cada turno. Su escenario imita una sesión realista en un monorepo de iOS. El conjunto de herramientas tiene 16 herramientas del harness y 17 herramientas MCP servidas por Xcode, GitHub y Linear (19.140 tokens). El resto es un preámbulo del harness, un CLAUDE.md de 200 líneas, un archivo de memoria y un par de skills que se cargan al arrancar (17.700 tokens). Cada turno empieza con ese prefijo compartido de 36.840 tokens, y después vienen 40 turnos del usuario separados por 45 segundos.
Entonces ocurren siete sucesos, de los que pasan en equipos reales. En el turno dos un compañero reescribe el primer mensaje. El turno ocho carga una skill de red, insertada en el prompt de sistema. En el turno 14 el servidor MCP de GitHub se reconecta y vuelve a registrar sus ocho herramientas al final de la lista. Antes del turno 20 hay una comida de 22 minutos. El turno 26 trae un cambio fusionado en CLAUDE.md, el 33 una skill de testing recién cargada, y en el 37 un cliente que se reconecta vuelve a renderizar la definición de la herramienta Bash, que crece de 1.450 a 1.480 tokens.
En conjunto, la sesión acierta el 85,6 % de las veces, una cifra que en un dashboard no haría que nadie mirase dos veces. El libro de costes cuenta otra historia. La entrada costó 4,65 $. Reutilizar a la perfección el prefijo anterior en cada turno habría costado 1,53 $, y sin caché ninguna, 23,10 $. Siete fallos suponen 3,41 $ de la factura, y los otros 33 turnos, 1,24 $ entre todos.
Ordenado por dólares por encima del ideal, el mayor culpable es el re-renderizado del esquema de una herramienta, con 0,91 $. Le siguen tres cambios distintos del prompt de sistema, 0,88 $ en total, luego la pausa para comer con 0,69 $, la reordenación de herramientas con 0,64 $ y, muy por detrás, la reescritura del compañero con 0,002 $.
Dos elementos de ese ranking cambian la idea de dónde sale el coste. Treinta tokens de más en el esquema de Bash hicieron que el sistema reenviara los 75.249 tokens de la petición, porque las herramientas encabezan la jerarquía. Solo con reordenar las herramientas de GitHub, sin tocar el esquema ni un byte, pasó lo mismo con 52.648 tokens. Mientras tanto, el bug que se ganó una entrada en el changelog, el compañero reescribiendo el mensaje cero, resultó ser el fallo más barato: solo volvió a cachear 157 tokens porque los mensajes están abajo del todo. El harness arregló el fallo que podía ver, mientras que los caros salen de tu propia configuración.
La lógica del diagnóstico es sencilla: gana lo primero, en orden de hash, que rompería el prefijo.
// Abridged from MissDiagnoser.swift: the first thing in hash order that would break the prefix wins.
public static func diagnose(previous: ContextSnapshot?, current: ContextSnapshot,
elapsed: TimeInterval, ttl: CacheTTL) -> MissCause {
guard let previous else { return .none } // cold start, not a miss
if toolsDiffer(previous, current) { return .toolSetChanged(...) }
if previous.tools.map(\.name) != current.tools.map(\.name) { return .toolOrderChanged }
if previous.system != current.system { return .systemSegmentChanged(segments: changed) }
if let i = firstRewrittenMessage(previous, current) { return .messagePrefixRewritten(atIndex: i) }
return elapsed > ttl.seconds ? .idlePastTTL(idleSeconds: elapsed) : .none
}
Trata el prefijo como una clave de caché
Repite la tarde con un prefijo diseñado, el escenario stabilized. El orden de las herramientas es fijo y los esquemas quedan congelados toda la sesión. Las skills entran en la conversación como mensajes con rol system y no como texto insertado en el prompt de sistema de nivel superior, lo que según la documentación de Fable 5.1 deja intactas las cachés de system y de mensajes. La fusión de CLAUDE.md se aplaza hasta un límite de sesión. La comida se queda como está, porque un TTL es una decisión de política y no un defecto del prefijo.
Esa ejecución tiene un solo fallo, la pausa para comer. Tasa de aciertos: 94,2 %. Coste: 2,22 $, es decir, 0,69 $ por encima del ideal. Todos los arreglos cambiaron lo que envía el harness; ninguno tocó el comportamiento del modelo.
La palanca que queda es el TTL. Pasa la ejecución estabilizada a entradas de una hora y no queda ningún fallo, con un coste de 2,11 $. Eso ahorra solo 11 céntimos frente a la versión de cinco minutos, porque cada token nuevo cuesta ahora 20 $ de escritura en vez de 12,50 $, y una tarde de ese recargo suma más o menos lo que una pausa para comer. El TTL largo solo compensa los días con huecos, así que fíjalo por equipo y no de forma global.
Lo que más dinero mueve es dónde va la skill. La misma skill, los mismos 5.200 tokens, y solo cambia el sitio:
// Same skill, same 5,200 tokens. Only the placement changes.
.loadSkill(name: "skill:networking-layer", tokens: 5_200, placement: .systemPrompt)
// → system prompt changed: re-sends the transcript after it
.loadSkill(name: "skill:networking-layer", tokens: 5_200, placement: .appendedMessage)
// → hit: costs only its own 5,200 tokens, once
Qué medir
Planifica el prefijo compartido como el tiempo de arranque de una app. Las herramientas y skills que tu monorepo carga por defecto suman 36.840 tokens que se hashean en cada turno, y una tasa de aciertos promedia toda la sesión y esconde en qué turno se quemó el dinero. La cifra que merece estar en un dashboard es dólares por encima del ideal, por causa y por día. /cost ya te da la causa, y el libro de costes, los dólares.
Una tasa de aciertos es un promedio, pero un fallo de caché es un suceso, así que presupuesta los sucesos.
El escenario es un modelo y no telemetría, y sus constantes son suposiciones, así que con tu propia lista de herramientas y tamaños de prefijo el ranking se moverá. La forma general, no. Las causas en lo alto de la jerarquía son las que más cuestan, y ningún changelog te las va a arreglar. El simulador, los escenarios, 30 tests y una vista SwiftUI del libro de costes están en el repositorio de demo del proyecto, cuyo target Demo aún no se había ejecutado en un simulador cuando se publicó.