Alternar tema

Cache no GitHub Actions: acelere pipelines de CI/CD em até 5x

Easton editorial illustration: fault-isolation scanner

O npm install levava 3 minutos e 15 segundos.

Esse era o tempo de build da CI em um projeto que assumi no ano passado. A cada push, eu ficava olhando o log do GitHub Actions girar, esperando aparecer aquele check verde. Para ser sincero, naquela época eu sempre mudava para outra janela para matar o tempo — afinal, não havia nada a fazer além de esperar.

Depois que adicionei o cache, o mesmo build passou a terminar em 40 segundos. Quase 5 vezes mais rápido.

Não é mágica. É apenas uma estratégia de cache do GitHub Actions configurada corretamente. A seguir, reuni as armadilhas que encontrei, os dados que medi e modelos de configuração que você pode copiar e usar. Se você também vive esperando a CI terminar, isso pode economizar um bom tempo.

1. Conceitos centrais do mecanismo de cache

Primeiro, vale entender como o cache funciona. Sem isso, fica fácil errar na configuração.

O mecanismo de cache do GitHub Actions é bem simples e tem três etapas: buscar → restaurar → salvar. Você define uma key, e o GitHub procura um cache correspondente. Se encontrar, restaura o conteúdo diretamente no diretório de trabalho; se não encontrar, salva um novo cache quando o job termina.

Mas há alguns limites rígidos que você precisa conhecer:

LimiteValor
Limite de cache por repositório10 GB
Limite por arquivo de cache5 GB (na prática, arquivos acima de 1 GB já costumam causar problemas)
Período de retençãoExclusão após 7 dias sem acesso
Limite global de uploads simultâneosNo máximo 5 caches enviados ao mesmo tempo

Já vi gente esbarrar no limite de 10 GB: o projeto tinha dependências demais, o cache crescia sem parar e, no fim, os caches novos não eram salvos enquanto os antigos eram removidos. Cada build virava uma inicialização a frio.

Outro ponto que costuma confundir: Cache e Artifact não são a mesma coisa. Cache serve à CI e prioriza velocidade. Artifact é destinado às pessoas, como os artefatos de build e relatórios de teste, e precisa ficar armazenado por mais tempo. O Cache tem limite de 10 GB; o Artifact não tem esse limite, embora consuma o armazenamento do repositório.

Também existe o Docker Layer Cache, específico para builds Docker. A lógica é um pouco diferente do cache comum e terá uma seção própria mais adiante.

2. Estratégias para projetar chaves de cache

A taxa de acerto do cache depende inteiramente do desenho da key. Esse é o ponto central de toda a estratégia.

O que é hashFiles()

O GitHub oferece a função integrada hashFiles(), que calcula o hash de um arquivo. Ela costuma ser usada com package-lock.json ou yarn.lock: se as dependências não mudarem, o hash também não muda e o cache pode ser reutilizado.

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

Esse trecho gera uma chave no formato npm-Linux-a1b2c3d4e5f6.... Enquanto o package-lock.json não mudar, a chave permanece igual.

restore-keys: uma alternativa de recuperação

As dependências acabam mudando, e é aí que entra restore-keys. Ele funciona como um mecanismo de correspondência gradual:

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

Primeiro, o GitHub tenta encontrar a key completa. Não encontrou? Então procura um cache antigo cujo nome comece com npm-Linux-. Mesmo sem um acerto exato, a maioria dos pacotes já estará em node_modules, e só será necessário instalar as novas dependências de forma incremental.

Comparação entre três padrões de chave

Nos meus testes, estes são os três padrões que recomendo:

Padrão simples (para projetos pequenos):

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

Padrão com versão (para várias versões do Node):

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

Padrão com vários caminhos (para monorepos):

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

Como saber se houve acerto de cache

actions/cache expõe uma variável de saída chamada 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 indica um acerto exato; false, um acerto parcial ou uma falha completa. Você pode usar essa variável para decidir se deve executar npm ci:

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

3. Exemplos práticos de configuração

Com a teoria esclarecida, vamos ao código. Testei todas as configurações abaixo, e elas podem ser copiadas diretamente.

Cache do npm (prefira setup-node)

Na verdade, o setup-node já oferece cache integrado e é mais simples do que configurar actions/cache manualmente:

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

Uma linha resolve. Mas, se você quiser armazenar outro diretório em cache, como node_modules, ainda precisará usar actions/cache:

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

Minha recomendação: prefira o cache integrado do setup-node, a menos que você tenha uma necessidade específica.

yarn e pnpm

O diretório de cache do yarn é diferente do npm:

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

O pnpm tem outra particularidade: ele usa um 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') }}

Cache do Python/pip

Este é o caminho de cache para projetos Python:

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

Docker Layer Cache

O build de imagens Docker costuma ser a etapa mais demorada. A boa notícia é que o BuildKit oferece suporte ao backend de cache do 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 indica que as camadas Docker devem ser armazenadas no serviço de cache do GitHub Actions. Nos meus testes, o build de uma imagem que levava 5 minutos caiu para cerca de 1 minuto.

Cache de módulos Go

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

Cache do Rust Cargo

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

A compilação do Rust é demorada, e o cache pode economizar bastante tempo. Mas cuidado: o diretório target cresce continuamente, então é recomendável limpá-lo periodicamente.

4. Otimização de desempenho e boas práticas

Reuni alguns dados de testes e armadilhas que encontrei para ajudar você a evitar os mesmos problemas.

Dados de benchmark

Segundo o relatório de testes da RunsOn, atualizado em janeiro de 2026, uma configuração adequada de cache produz os seguintes resultados:

OperaçãoSem cacheCom cacheGanho
npm install3 minutos40 segundoscerca de 5x
yarn install2 minutos e 30 segundos35 segundoscerca de 4x
Docker build5 minutos1 minutocerca de 5x
pip install45 segundos8 segundoscerca de 5x

A taxa de acerto fica entre 70% e 90%, dependendo da qualidade da sua estratégia de chaves.

Armadilhas comuns

Não armazene node_modules diretamente no cache

Foi exatamente o que fiz no início, e acabei enfrentando um problema sério.

# Não faça isso
path: node_modules

node_modules depende da plataforma: pacotes instalados no Linux podem apresentar problemas no Windows. O correto é armazenar o diretório de cache global, como ~/.npm, e deixar o npm ci montar as dependências.

Use GNU tar + zstd para cache entre sistemas operacionais

Por padrão, o formato de tar varia entre macOS e Windows, o que pode causar falhas na restauração do cache. Adicione esta configuração:

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

Problemas de cache contaminado

Às vezes, o cache contém uma dependência com problema e faz o build falhar continuamente. Há duas soluções:

  1. Exclua o cache manualmente: abra a página Actions → Caches do repositório no GitHub e clique em excluir
  2. Force a atualização da key: adicione um prefixo ou timestamp para gerar uma nova chave
key: npm-v2-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}

Checklist de boas práticas

Para encerrar, aqui estão alguns pontos que você pode conferir antes de configurar:

  1. Dê preferência ao cache integrado das actions oficiais (setup-node, setup-python)
  2. Inclua hashFiles na key; caso contrário, o cache antigo continuará sendo usado mesmo após uma atualização das dependências
  3. Configure restore-keys; a correspondência gradual pode salvar o build
  4. Não armazene node_modules no cache; armazene o diretório global
  5. Exclua caches antigos periodicamente para não ultrapassar o limite de 10 GB

5. Perguntas frequentes

P1: Por que minha taxa de acerto de cache é baixa?

O motivo mais comum é uma key que muda com frequência. Se você inclui um timestamp ou o nome da branch, por exemplo, cada push gera uma key nova. A solução é usar apenas runner.os e hashFiles, removendo variáveis desnecessárias.

Outro motivo é hashFiles incluir arquivos que não deveria. Se você usa hashFiles('**/*.json'), uma simples mudança em um arquivo de configuração invalida o cache. Restrinja a correspondência a package-lock.json ou yarn.lock.

P2: O que fazer quando o espaço de cache ultrapassa o limite?

10 GB parece muito, mas um monorepo ou cache Docker pode consumir esse espaço rapidamente. Soluções:

  1. Limpeza periódica: exclua manualmente os caches antigos em GitHub Actions → Caches
  2. Caches separados: use uma key diferente para cada tipo de dependência, em vez de guardar tudo em um único cache
  3. Use self-hosted runners: eles não têm o limite de 10 GB

P3: self-hosted runners exigem alguma configuração especial?

Não. O mecanismo de cache funciona da mesma forma. A vantagem dos self-hosted runners é que o cache fica armazenado localmente, sem latência de transferência pela rede, o que acelera a restauração. A desvantagem é que o cache não é limpo automaticamente; você precisa criar um script para fazer essa limpeza periodicamente.

P4: Como forçar uma atualização do cache?

Altere a key. Adicione um número de versão como prefixo:

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

Outra opção é excluir o cache antigo para que o sistema gere um novo.

Conclusão

Depois de tudo isso, a ideia central cabe em uma frase: um cache bem configurado pode deixar a CI 5 vezes mais rápida.

Vamos fazer as contas: se cada build economiza 2 minutos e você executa 10 builds por dia, são 600 minutos por mês, ou cerca de 10 horas. Tempo suficiente para escrever vários artigos.

Se você está começando com GitHub Actions, use primeiro o cache integrado do setup-node. Uma linha de configuração basta. Quando encontrar um gargalo, volte para estudar estratégias de chave mais avançadas e o Docker Layer Cache.

Este é o terceiro artigo da série de guias práticos de GitHub Actions. Os anteriores abordam a criação de pipelines de CI e estratégias de implantação; se o tema interessar, vale consultar os artigos anteriores.

No próximo push, observe o tempo do build. Veja se ele cai de 3 minutos para 40 segundos — basta testar.

Configurar o cache do GitHub Actions para acelerar o CI/CD

Configure o cache do GitHub Actions para reduzir o tempo do npm install de 3 minutos para 40 segundos

⏱️ Estimated time: 10 min

  1. 1

    Step 1: Escolher a estratégia de cache

    Escolha a estratégia de cache de acordo com o gerenciador de pacotes do projeto:

    • Projetos npm: dê preferência ao cache integrado do setup-node
    • Projetos yarn/pnpm: configure o caminho do cache
    • Builds Docker: use o backend gha do BuildKit
  2. 2

    Step 2: Projetar a chave de cache

    Use hashFiles() com base no arquivo de lock para gerar uma chave estável:

    • Padrão básico: {{ runner.os }}-node-{{ hashFiles('**/package-lock.json') }}
    • Adicione restore-keys como correspondência alternativa
    • Evite incluir timestamp ou nome de branch na key
  3. 3

    Step 3: Adicionar a configuração de cache

    Adicione uma etapa de cache ao arquivo de workflow:

    • npm: use actions/setup-node@v4 com cache: 'npm'
    • Caminhos personalizados: use actions/cache@v4
    • Docker: configure cache-from e cache-to
  4. 4

    Step 4: Validar o efeito do cache

    Verifique se houve acerto de cache:

    • Consulte a variável de saída cache-hit (true indica acerto exato)
    • Compare o tempo de build (a redução esperada é de 4 a 5 vezes)
    • Confira a página Actions → Caches para confirmar que o cache foi armazenado
  5. 5

    Step 5: Fazer a manutenção periódica do cache

    Evite problemas com o cache:

    • Monitore o espaço usado pelo cache (limite de 10 GB)
    • Exclua caches antigos periodicamente
    • Em caso de cache contaminado, altere o prefixo da key para forçar a recriação

FAQ

Por que minha taxa de acerto de cache é de apenas 30%?
Geralmente, o problema está no desenho da key. Verifique se ela inclui variáveis que mudam com frequência, como timestamp ou nome de branch, e passe a usar apenas runner.os e hashFiles. Confirme também se o caminho em hashFiles corresponde exatamente ao arquivo de lock, sem curingas que incluam arquivos demais.
O que acontece quando o cache ultrapassa 10 GB?
O GitHub exclui automaticamente os caches mais antigos para liberar espaço. Recomendações:

• Separe tipos diferentes de dependência em caches distintos, com uma key para npm, outra para Docker e outra para pip
• Exclua periodicamente caches desnecessários na página Actions → Caches
• Em projetos monorepo, avalie separar repositórios ou usar self-hosted runners
Branches diferentes podem compartilhar o cache?
Por padrão, o cache é compartilhado apenas entre a branch atual e a branch padrão, como main ou master. Para compartilhar entre branches, remova o nome da branch da key e use somente um hash baseado em arquivos. restore-keys também pode ajudar a encontrar caches de outras branches.
O que muda no cache de um self-hosted runner?
O mecanismo é o mesmo, mas há duas diferenças: a vantagem é que o cache fica armazenado localmente e é restaurado mais rápido, sem latência de rede; a desvantagem é que não há limite de 10 GB nem limpeza automática, então você precisa criar um script para excluir caches antigos periodicamente.
Uma falha ao restaurar o cache interrompe o build?
Não. O cache é uma otimização opcional, e uma falha na restauração não impede o build. O GitHub Actions continua executando as etapas seguintes, mas baixa as dependências novamente nessa execução. O log mostra 'Cache not found for key: xxx', e um novo cache é salvo automaticamente para a próxima execução.
Como saber quando o cache precisa ser atualizado?
Há três situações em que o cache precisa ser atualizado:

• Mudança na versão das dependências: hashFiles cuida disso automaticamente, sem intervenção manual
• Cache contaminado: o build começa a falhar de repente e é preciso excluir o cache antigo
• Mudança de configuração: em uma atualização do Node, por exemplo, inclua o número da versão na key

Na maioria dos casos, uma configuração correta dispensa gerenciamento manual.

9 min de leitura · Publicado em: 7 abr 2026 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog