Buenas prácticas de caché: determinismo, aislamiento y disciplina de fronteras
Tras el mecanismo —hashing, aciertos, outputs, seguridad— viene la disciplina que lo convierte en una caché en la que se puede confiar. Cuatro prácticas condensan todo lo aprendido: entradas deterministas para que el mismo input dé siempre el mismo output, aislamiento por rama para separar contextos de confianza, auditoría de qué entra en el hash para no equivocarse ni por exceso ni por defecto, y el criterio para no cachear aquello que no es una función pura de sus entradas.
Después de entender qué compone un hash, qué invalida un acierto, cómo se restauran los outputs y por dónde se envenena una caché, queda destilar todo eso en hábitos. Las buenas prácticas de caching no son una lista de recetas sueltas: son corolarios directos del mecanismo. Cada una responde a una de las formas en que una caché puede traicionarte —mintiendo sobre cuándo reutilizar, entregando artefactos mutilados o dejando que un desconocido escriba en ella— y las convierte en disciplina. Una caché no vale por lo que guarda, sino por la honestidad de sus entradas y el rigor de sus fronteras.
- Exigir entradas deterministas y erradicar las fuentes de no determinismo que envenenan la clave.
- Auditar qué entra en el hash y equilibrar los dos errores simétricos: incluir de más y de menos.
- Aplicar aislamiento por rama para que la confianza no se filtre entre contextos.
- Distinguir qué tareas merecen caché y cuáles nunca deben cachearse.
Entradas deterministas
El determinismo es la precondición de toda la caché: si las mismas entradas no producen la misma salida bit a bit, el contrato “misma clave, mismo resultado” es falso desde la raíz. Una tarea no determinista no solo impide aciertos —recordemos del nivel del miss que su clave cambia sin que nadie la toque—; peor aún, si su salida alimenta a otra tarea, contamina el hash río abajo y propaga la inestabilidad por todo el grafo.
Las fuentes de no determinismo son pocas y conocidas, y casi todas tienen remedio estándar:
- Timestamps incrustados en el artefacto: fíjalos con un origen reproducible, como
SOURCE_DATE_EPOCH, en vez de dejar que cada corrida estampe la hora actual. - Rutas absolutas que dependen de dónde vive el repo: emite rutas relativas para que dos máquinas produzcan el mismo output.
- Orden no estable: ordena explícitamente las listas de archivos, las claves de un objeto o las entradas de un manifiesto, en vez de confiar en el orden del sistema de archivos.
- Locale y zona horaria: fíjalos, porque cambian la ordenación y el formato de fechas y números.
- Acceso a red en tiempo de build: una descarga puede devolver algo distinto cada vez; conviértela en una entrada explícita y fijada.
# Fija las fuentes de no determinismo mas comunes antes de construir.
export SOURCE_DATE_EPOCH=1700000000
export TZ=UTC
export LC_ALL=C.UTF-8
El norte conceptual es la construcción reproducible: la meta de que cualquiera, en cualquier máquina, partiendo de las mismas entradas, obtenga byte a byte el mismo artefacto. La reproducibilidad no es solo higiene de caché; es también auditabilidad y seguridad de la cadena de suministro. Pero para la caché es, sencillamente, la condición sin la cual nada de lo demás se sostiene.
Fijar la cadena de herramientas
Las entradas deterministas no son solo tus archivos: también lo es la versión de todo lo que los transforma. Un mismo código compilado con dos versiones distintas del bundler, del compilador de tipos o de un plugin puede producir artefactos distintos, así que la cadena de herramientas es, a efectos de reproducibilidad, una entrada más. Fíjala con la misma disciplina que las dependencias: una versión exacta del gestor de paquetes, del runtime y de los plugins de build, para que dos máquinas no diverjan solo porque una tenía algo más nuevo. Una herramienta sin fijar es una fuente de no determinismo que ninguna corrida en seco delata hasta que dos entornos discrepan y nadie entiende por qué.
Verificar que de verdad es reproducible
Declarar la intención de ser determinista no basta; hay que comprobarlo. La prueba es mecánica y contundente: construye el mismo estado de fuentes dos veces en condiciones limpias y compara los artefactos byte a byte. Si difieren, tienes no determinismo residual, y el diff de ambos artefactos suele señalar la fuente —un timestamp aquí, una ruta absoluta allá, un orden que bailó—. Es el mismo espíritu de la técnica de las dos corridas que usaste para depurar un miss, aplicado ahora al output en vez de al hash: dos ejecuciones que deberían coincidir, y la diferencia como confesión de lo que aún no controlas.
Un build que crees reproducible y no has verificado es una hipótesis, no un hecho. Añade a tu CI un chequeo periódico que construya dos veces y compare: es barato, corre desatendido y caza regresiones de determinismo el día que entran, no meses después cuando la caché empieza a comportarse raro. La reproducibilidad sin verificar tiende a degradarse sola, porque cualquier dependencia o herramienta nueva puede reintroducir una fuente de no determinismo sin avisar.
Auditar qué entra en el hash
Del nivel del hashing arrastramos una asimetría que aquí se vuelve práctica de revisión: hay dos maneras de equivocarse, y no son igual de graves. Incluir de más en el hash peca por rendimiento —invalidas cuando no hacía falta y pagas recomputes que sobraban—. Incluir de menos peca por corrección —aciertas cuando no debías y sirves un artefacto construido con una entrada que cambió sin que la clave se enterara—. La primera cuesta segundos; la segunda, un despliegue equivocado.
Corrida en seco
Inspecciona el desglose del hash sin ejecutar nada. Es la lente para ver qué entra de verdad, no lo que crees que entra.
Env como allowlist
Declara solo las variables que la tarea consume. Una lista explícita evita que un valor efímero se cuele en la clave.
Globs afilados
Haz que tus patrones de inputs cubran lo que la tarea lee y ni un archivo más: nada de generados, logs ni temporales.
La config es código
La declaración de inputs y outputs merece revisión en el PR igual que el código: un glob mal puesto es un bug latente.
La disciplina concreta es tratar la declaración de entradas como código que se audita, no como configuración que se copia y se olvida. Cuando una tarea empieza a leer una fuente nueva —un archivo, una variable, una herramienta del sistema—, la pregunta refleja debe ser “¿lo ve el hash?”. Y la herramienta para responderla es siempre la corrida en seco: si el desglose no menciona algo que la tarea consume, tienes una fuga de corrección; si menciona algo que la tarea ignora, tienes una fuga de rendimiento. Auditar no es desconfiar de la caché, es mantener afilada la teoría de entradas que la gobierna.
Un flujo de auditoría que cabe en la cabeza
La auditoría no tiene por qué ser un ritual pesado. Cabe en tres gestos: primero, una corrida en seco que imprima el desglose del hash de la tarea; segundo, contrastar ese desglose con lo que sabes que la tarea consume —¿está todo lo que lee?, ¿sobra algo que ignora?—; tercero, ajustar la declaración y repetir hasta que la cuenta cuadre. Hecho una vez al definir la tarea y revisado cuando cambia lo que consume, ese flujo mantiene honesta la teoría de entradas sin convertirse en burocracia.
Cuando dudes entre incluir o no una entrada en el hash, inclúyela. Las herramientas hacen exactamente eso por defecto —cachean incluyéndolo casi todo y te dejan restar solo lo que demuestres irrelevante— porque la asimetría manda: un falso miss se nota y cuesta segundos, un falso hit se esconde y cuesta un artefacto malo desplegado. Optimiza la caché quitando entradas solo cuando hayas probado, con la corrida en seco, que de verdad no afectan a la salida. Empezar prieto y aflojar con evidencia es seguro; empezar laxo y apretar tras un incidente es aprender por las malas.
Aislamiento por rama
Del nivel de seguridad heredamos una práctica no negociable: una caché compartida cruza fronteras de confianza, y esas fronteras hay que hacerlas explícitas. En la práctica significa no dar permiso de escritura al mismo ámbito desde contextos de confianza distinta —que un PR de un fork lea de la caché para acelerar, pero escriba solo en un ámbito aislado del que las ramas protegidas nunca leen—. El principio, recordémoslo, es que el hash prueba qué es un artefacto pero jamás quién tuvo derecho a crearlo; la confianza vive en la topología del almacén, no en la clave.
De aquí se deriva una regla de mínimo privilegio para la escritura: concede permiso de escribir en la caché solo a los contextos cuya autoridad estás dispuesto a heredar. En CI, eso suele traducirse en dar a las corridas de PRs no confiables una caché de solo lectura —aceleran sin poder envenenar— y reservar la escritura a las ramas protegidas, donde el código ya pasó revisión. La lectura puede ser ancha porque leer no compromete a nadie; la escritura tiene que ser estrecha porque escribir dicta lo que otros consumirán.
Qué no cachear
Hay una práctica igual de importante y mucho más olvidada: no todo debe cachearse. La caché reutiliza el resultado de una función pura de sus entradas; en cuanto una tarea deja de ser esa función pura, cachearla es un error de categoría, no de configuración.
flowchart TD T[Una tarea candidata a cache] --> Q1[Es una funcion pura de sus entradas] Q1 -->|no, tiene efectos o lee secretos| No[No la caches] Q1 -->|si, es pura| Q2[Sus entradas estan declaradas y son deterministas] Q2 -->|no| Fix[Arregla las entradas antes de cachear] Q2 -->|si| Yes[Cachea con confianza] style Yes fill:#a6e3a1,color:#11111b style No fill:#f38ba8,color:#11111b style Fix fill:#f9e2af,color:#11111b
Las tareas que no deben cachearse comparten un rasgo: su valor está en el efecto, no en el artefacto. Un deploy o un publish cuya gracia es hacer algo en el mundo —empujar a un servidor, subir un paquete— no puede saltarse con un hit, porque saltarlo significa no desplegar. Una tarea que lee secretos no debe congelar su salida bajo una clave que podría filtrarlos a un almacén compartido. Y un test inestable —que a veces pasa y a veces no sin que cambien sus entradas— no es determinista y, por tanto, no cumple la precondición de la caché: cachear su verde de hoy es enterrar su rojo de mañana.
| Tarea | ¿Cachear? | Por qué |
|---|---|---|
| build, compilación, lint, test estable | sí | función pura de entradas hacia artefactos restaurables |
| deploy, publish | no | su valor es un efecto en el mundo, no un artefacto |
| tarea que lee o escribe secretos | no | congelaría el secreto bajo una clave compartida |
| test inestable | no | no es determinista y enterraría fallos reales |
La regla que ordena la tabla es una sola: cachea lo que sea una función pura de entradas declaradas hacia salidas restaurables, y solo eso. Todo lo demás —lo que actúa sobre el mundo, lo que toca secretos, lo que no es determinista— o bien no se cachea, o bien se arregla hasta que pueda cachearse con honestidad. Desactivar la caché en esas tareas no es renunciar a rendimiento: es reconocer que reutilizar un efecto no es reutilizar, es no hacerlo.
El error más silencioso de esta familia es cachear una tarea cuyo verdadero producto es un efecto secundario. La caché ve que la tarea terminó en cero, guarda sus archivos y su código de salida, y en el siguiente hit “reproduce” el éxito sin volver a producir el efecto. Con un build eso es exactamente lo que quieres. Con un despliegue es un desastre: la caché te dice que desplegaste porque una vez desplegaste con esas mismas entradas, cuando en realidad no ha salido un solo byte hacia producción. Antes de cachear una tarea, pregúntate si su resultado es un artefacto que se pueda restaurar o un efecto que haya que volver a ejecutar. Solo lo primero es cacheable.
Si algo une las cuatro prácticas de este nivel, es que ninguna es un truco de configuración: todas son formas de una misma exigencia, la de tratar cada tarea como lo que la caché supone que es —una función pura de unas entradas declaradas hacia unas salidas restaurables— y de defender esa suposición contra las mil maneras en que la realidad conspira para romperla. El determinismo defiende que las mismas entradas den la misma salida, sin lo cual la clave miente desde el primer día. La auditoría del hash defiende que la cuenta de entradas coincida con lo que la tarea de verdad consume, ni un input de más que malgaste recomputes ni uno de menos que reparta artefactos rancios. El aislamiento por rama defiende que la frontera de confianza del almacén sea explícita, porque el hash certifica el contenido pero calla sobre su autor. Y el criterio de qué no cachear defiende la suposición más básica de todas: que la tarea sea, siquiera, una función y no un efecto disfrazado de función. Vistas juntas, las cuatro dibujan una manera de pensar que vale mucho más que cualquier receta concreta de Turborepo, Nx o Bazel, porque las herramientas cambian y esto no: una caché es la aplicación disciplinada de una teoría sobre qué determina el resultado de tu trabajo y sobre quién tiene derecho a hablar en nombre de ese resultado. Cuando esa teoría es honesta —entradas completas y deterministas, fronteras de confianza explícitas, tareas que de verdad son funciones—, la caché acelera sin mentir jamás, y llegas a confiar en ella tanto que dejas de pensarla. Cuando la teoría tiene un agujero —una entrada callada, un contexto de confianza mezclado, un efecto cacheado como si fuera artefacto—, la caché no falla ruidosamente sino que sirve basura con absoluta convicción, y descubres el agujero por el peor camino posible: un despliegue que no coincide con el código, un secreto en un bucket compartido, un rojo enterrado bajo un verde viejo. Interiorizar estas prácticas es dejar de padecer las cachés para empezar a merecerlas.
- Audita una tarea con la corrida en seco y comprueba que todo lo que consume aparece en el desglose y nada ajeno se cuela.
- Caza una fuente de no determinismo —un timestamp, una ruta absoluta, un orden inestable— y fíjala hasta que dos corridas den el mismo hash.
- Convierte tu declaración de
enven una allowlist mínima y verifica en el desglose que ninguna variable efímera entra en la clave. - Revisa que tu caché remota aísla por rama: los PRs escriben en un ámbito propio y las ramas protegidas no leen de él.
- Recorre tus tareas y marca cuáles no deben cachearse —deploy, publish, lectura de secretos, tests inestables— y desactiva la caché en ellas con una razón escrita para cada una.