Cambiar tema

Estrategia de caché en GitHub Actions: acelera tu pipeline CI/CD hasta 5 veces

Easton editorial illustration: fault-isolation scanner

npm install: 3 minutos y 15 segundos.

Ese era el tiempo de build CI de un proyecto que asumí el año pasado. Cada push, miraba los logs de GitHub Actions dando vueltas, esperando el tick verde. La verdad es que a menudo cambiaba de ventana a otra cosa — total, había que esperar.

Después añadí caché. El mismo build, 40 segundos. Unas 5 veces más rápido.

No es magia: es configurar bien la estrategia de caché en GitHub Actions. En este artículo recopilo los errores que cometí, los datos que probé y plantillas de configuración listas para copiar. Si también esperas builds CI, esto puede ahorrarte bastante tiempo de café.

1. Conceptos centrales del mecanismo de caché

Primero entiende cómo funciona la caché; si no, es fácil tropezar al configurarla.

El mecanismo de caché en GitHub Actions es bastante simple — tres pasos: buscar → restaurar → guardar. Defines una key, GitHub busca si hay coincidencia entre todas las cachés. Si la encuentra, restaura directamente en tu directorio de trabajo; si no, al terminar la tarea guarda una nueva.

Pero hay límites duros que debes conocer:

LímiteValor
Tope de caché por repositorio10 GB
Tope por archivo de caché5 GB (por encima de 1 GB suelen aparecer problemas)
Plazo de retenciónEliminación tras 7 días sin acceso
Límite global de subidas concurrentesMáximo 5 cachés subiendo a la vez

He visto gente caer en el límite de 10 GB — demasiadas dependencias, la caché crece y crece, al final la nueva no cabe, la antigua se borra y cada build es un «arranque en frío».

Otro punto que confunde: Cache y Artifact no son lo mismo. Cache es para CI, busca velocidad; Artifact es para personas, p. ej. artefactos de build o informes de tests, con retención larga. Cache tiene límite de 10 GB; Artifact no tiene tope (pero consume almacenamiento del repo).

También está Docker Layer Cache, específico para builds Docker, con lógica distinta a la caché normal; lo veremos aparte más adelante.

2. Estrategia de diseño de claves de caché

Si la caché acierta depende enteramente de si la key está bien diseñada. Es el núcleo de toda la estrategia.

Qué es hashFiles()

GitHub ofrece la función integrada hashFiles(), que calcula el hash de archivos. Se usa mucho en package-lock.json o yarn.lock — si las dependencias no cambian, el hash tampoco y la caché acierta.

key: npm-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}

Esto genera una clave del tipo npm-Linux-a1b2c3d4e5f6.... Mientras package-lock.json no cambie, la clave tampoco.

restore-keys: plan B

Pero las dependencias se actualizan, y ahí entra restore-keys. Es un mecanismo de «coincidencia degradada»:

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-{{ runner.os }}-

Prioriza la key completa. ¿No coincide? Busca cachés antiguas que empiecen por npm-Linux-. No es acierto total, pero la mayoría de paquetes en node_modules ya están; solo instalas dependencias nuevas de forma incremental.

Comparativa de tres patrones de nomenclatura

Tras probar, recomiendo estos tres:

Modo simple (proyectos pequeños):

key: {{ runner.os }}-node-{{ hashFiles('**/package-lock.json') }}

Modo versión (varias versiones de Node):

key: {{ runner.os }}-node{{ matrix.node-version }}-{{ hashFiles('**/package-lock.json') }}

Modo multi-ruta (monorepo):

key: {{ runner.os }}-{{ hashFiles('**/package-lock.json', '**/yarn.lock') }}

Cómo saber si la caché acertó

actions/cache expone la variable cache-hit:

- uses: actions/cache@v4
  id: cache-npm
  with:
    path: ~/.npm
    key: {{ runner.os }}-node-{{ hashFiles('**/package-lock.json') }}

- name: Check cache hit
  run: echo "Cache hit - {{ steps.cache-npm.outputs.cache-hit }}"

true = acierto exacto; false = acierto parcial o miss total. Puedes usar esa variable para decidir si ejecutar npm ci:

- name: Install dependencies
  if: steps.cache-npm.outputs.cache-hit != 'true'
  run: npm ci

3. Ejemplos de configuración en la práctica

Teoría hecha; al código. Estas configuraciones las he probado y puedes copiarlas directamente.

Caché npm (recomendado: setup-node)

setup-node ya trae caché integrada, más limpia que actions/cache manual:

- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'  # o 'yarn', 'pnpm'

Una línea. Si quieres cachear otros directorios (p. ej. node_modules), sigue haciendo falta actions/cache:

- uses: actions/cache@v4
  with:
    path: node_modules
    key: {{ runner.os }}-nm-{{ hashFiles('**/package-lock.json') }}
    restore-keys: {{ runner.os }}-nm-

Mi consejo: prioriza la caché integrada de setup-node, salvo necesidades especiales.

yarn y pnpm

El directorio de caché de yarn difiere de npm:

- uses: actions/cache@v4
  with:
    path: |
      ~/.yarn/cache
      ~/.yarn/install-state.gz
    key: yarn-{{ runner.os }}-{{ hashFiles('**/yarn.lock') }}

pnpm es más especial: usa un store global:

- uses: pnpm/action-setup@v4
  with:
    version: 9

- uses: actions/cache@v4
  with:
    path: ~/.pnpm-store
    key: pnpm-{{ runner.os }}-{{ hashFiles('**/pnpm-lock.yaml') }}

Caché Python/pip

Rutas de caché para proyectos Python:

- uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: pip-{{ runner.os }}-{{ hashFiles('**/requirements.txt') }}
    restore-keys: pip-{{ runner.os }}-

Docker Layer Cache

Los builds Docker son lo más lento. La buena noticia: BuildKit soporta el backend de caché de GitHub Actions:

- uses: docker/setup-buildx-action@v3

- uses: docker/build-push-action@v6
  with:
    context: .
    push: false
    cache-from: type=gha
    cache-to: type=gha,mode=max

type=gha usa el servicio de caché de GitHub Actions para capas Docker. En pruebas, un build de imagen de 5 minutos baja a unos 60 segundos.

Caché de módulos Go

- uses: actions/cache@v4
  with:
    path: |
      ~/go/pkg/mod
      ~/.cache/go-build
    key: go-{{ runner.os }}-{{ hashFiles('**/go.sum') }}

Caché Cargo (Rust)

- uses: actions/cache@v4
  with:
    path: |
      ~/.cargo/registry
      ~/.cargo/git
      target
    key: cargo-{{ runner.os }}-{{ hashFiles('**/Cargo.lock') }}

Rust compila lento; la caché ahorra mucho tiempo. Cuidado: el directorio target crece — conviene limpiarlo periódicamente.

4. Optimización de rendimiento y mejores prácticas

Recopilo datos probados y errores que cometí, para que evites los mismos.

Datos de referencia de rendimiento

Según el informe de pruebas de RunsOn (actualizado en enero de 2026), con caché bien configurada:

OperaciónSin cachéCon cachéMejora
npm install3 min40 s~5×
yarn install2 min 30 s35 s~4×
Docker build5 min1 min~5×
pip install45 s8 s~5×

La tasa de acierto ronda el 70-90%, según qué tan bien diseñes las claves.

Trampas habituales

No cachees node_modules directamente

Al principio lo hice así y fue un desastre.

# No hagas esto
path: node_modules

node_modules es específico de plataforma — paquetes instalados en Linux pueden fallar en Windows. Lo correcto es cachear el directorio global (~/.npm) y dejar que npm ci monte el árbol.

Caché multi-OS: GNU tar + zstd

El tar por defecto en macOS y Windows usa formatos distintos y la restauración falla. Añade:

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}
    enableCrossOsArchive: true

Contaminación de caché

A veces la caché guarda dependencias rotas y el build falla una y otra vez. Solución:

  1. Borrar manualmente: en el repo, Actions → Caches, eliminar
  2. Forzar nueva key: añade prefijo o marca de tiempo para regenerar
key: npm-v2-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}

Lista de mejores prácticas

Resumen final; revísala antes de configurar:

  1. Prioriza la caché integrada de actions oficiales (setup-node, setup-python)
  2. La key debe incluir hashFiles, si no, al actualizar dependencias seguirás usando la caché antigua
  3. Escribe restore-keys — la coincidencia degradada salva builds
  4. No cachees node_modules, cachea directorios globales
  5. Limpia cachés caducadas periódicamente, evita superar 10 GB

5. Preguntas frecuentes

P1: ¿Por qué mi tasa de acierto es baja?

La causa más común es una key que cambia demasiado. Por ejemplo, incluir marca de tiempo o nombre de rama: cada push genera una key nueva. Solución: solo runner.os y hashFiles, sin variables innecesarias.

Otra causa: hashFiles coincide con archivos que no debería. Si escribes hashFiles('**/*.json'), un cambio en config invalida la caché. Limita a package-lock.json o yarn.lock.

P2: ¿Qué hacer si se supera el espacio de caché?

10 GB parece mucho, pero monorepos o caché Docker lo agotan rápido. Soluciones:

  1. Limpieza periódica: GitHub Actions → Caches, borrar las antiguas
  2. Cachés separadas: distintas keys por tipo de dependencia
  3. Self-hosted runners: sin límite de 10 GB

P3: ¿Los self-hosted runners requieren configuración especial?

No; el mecanismo es el mismo. Ventaja: la caché es local, sin latencia de red, restauración más rápida. Inconveniente: no se limpia sola; necesitas scripts programados.

P4: ¿Cómo forzar actualización de caché?

Cambia la key. Añade un prefijo de versión:

key: npm-v3-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}

O borra la caché antigua y deja que el sistema regenere.

Resumen

En una frase: con buena caché, el CI puede ir 5 veces más rápido.

Una cuenta rápida — si cada build ahorra 2 minutos y corres 10 al día, en un mes son 600 minutos, unas 10 horas. Tiempo de sobra para escribir varios artículos.

Si acabas de empezar con GitHub Actions, empieza por la caché integrada de setup-node: una línea basta. Cuando llegues al límite, vuelve aquí para claves más complejas y Docker Layer Cache.

Este artículo es el n.º 3 de la serie Guía práctica de GitHub Actions. Antes escribí sobre montaje de pipelines CI y estrategias de despliegue; si te interesa, revisa artículos anteriores.

En el próximo push, mira el tiempo de build. ¿De 3 minutos a 40 segundos? Pruébalo y lo verás.

Configurar caché en GitHub Actions para acelerar CI/CD

Configura la caché en GitHub Actions para reducir el tiempo de npm install de 3 minutos a 40 segundos

⏱️ Estimated time: 10 min

  1. 1

    Step 1: Elegir el esquema de caché

    Elige el esquema según el gestor de paquetes del proyecto:

    • Proyectos npm: prioriza la caché integrada de setup-node
    • Proyectos yarn/pnpm: configura las rutas de caché
    • Builds Docker: usa el backend gha de BuildKit
  2. 2

    Step 2: Diseñar las claves de caché

    Usa hashFiles() basado en el archivo de bloqueo para generar claves estables:

    • Patrón básico: {{ runner.os }}-node-{{ hashFiles('**/package-lock.json') }}
    • Añade restore-keys como respaldo de coincidencia
    • Evita incluir marcas de tiempo o nombres de rama en la key
  3. 3

    Step 3: Añadir la configuración de caché

    Añade pasos de caché en el archivo workflow:

    • npm: usa actions/setup-node@v4 con cache: 'npm'
    • Rutas personalizadas: usa actions/cache@v4
    • Docker: configura cache-from y cache-to
  4. 4

    Step 4: Verificar el efecto de la caché

    Comprueba si la caché acierta:

    • Revisa la variable de salida cache-hit (true = coincidencia exacta)
    • Compara tiempos de build (deberían reducirse 4-5 veces)
    • Consulta Actions → Caches para confirmar que la caché se almacenó
  5. 5

    Step 5: Mantenimiento periódico de la caché

    Evita problemas de caché:

    • Monitoriza el uso de espacio (límite 10 GB)
    • Limpia cachés antiguas periódicamente
    • Si hay contaminación, actualiza el prefijo de la key para forzar reconstrucción

FAQ

¿Por qué mi tasa de acierto de caché es solo del 30%?
Suele ser un problema de diseño de la key. Comprueba si incluiste variables que cambian con frecuencia (marca de tiempo, nombre de rama); cámbialo a solo runner.os y hashFiles. Además, confirma que la ruta de hashFiles coincide exactamente con el archivo de bloqueo, evitando comodines que abarquen demasiados archivos.
¿Qué pasa si la caché supera los 10 GB?
GitHub limpia automáticamente las cachés más antiguas para liberar espacio. Recomendaciones:

• Separa la caché por tipo de dependencia (npm, Docker, pip, cada una con su key)
• Elimina manualmente cachés inútiles en Actions → Caches
• En monorepos, considera repositorios separados o self-hosted runners
¿Las ramas distintas pueden compartir caché?
Por defecto, la caché solo se comparte entre la rama actual y la rama por defecto (main/master). Si quieres compartir entre ramas, quita el nombre de rama de la key y usa solo el hash basado en archivos. Además, restore-keys puede ayudar a coincidir con cachés de otras ramas.
¿En qué se diferencia la caché en self-hosted runners?
El mecanismo es el mismo, pero hay dos diferencias: la ventaja es que la caché está en local y la restauración es más rápida (sin latencia de red); la desventaja es que no hay límite de 10 GB pero tampoco limpieza automática, debes escribir scripts para limpiar cachés antiguas.
¿Un fallo al restaurar la caché interrumpe el build?
No. La caché es una optimización opcional; un fallo de restauración no afecta al build. GitHub Actions continúa con los pasos siguientes, solo que ese build volverá a descargar dependencias. En los logs verás el aviso 'Cache not found for key: xxx' y se guardará una caché nueva para la próxima vez.
¿Cómo saber si la caché necesita actualizarse?
Hay tres situaciones que requieren actualizar la caché:

• Cambio de versiones de dependencias: hashFiles lo gestiona automáticamente, sin intervención manual
• Contaminación de caché: el build falla de repente y hay que limpiar la caché antigua
• Cambio de configuración: p. ej. actualización de Node, hay que incluir la versión en la key

En la mayoría de casos, con la configuración correcta no hace falta gestionarla manualmente.

8 min de lectura · Publicado el: 7 abr 2026 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog