Design wechseln

GitHub Actions Cache-Strategie: CI/CD-Pipeline 5× beschleunigen

npm install: 3 Minuten 15 Sekunden.

So lang war die CI-Build-Zeit eines Projekts, das ich letztes Jahr übernommen habe. Bei jedem Push starrte ich auf die GitHub Actions Logs und wartete auf den grünen Haken. Ehrlich gesagt wechselte ich oft zu anderen Tabs – Warten gehört dazu.

Mit Cache: derselbe Build in 40 Sekunden. Etwa fünfmal schneller.

Keine Magie – nur die richtige GitHub Actions Cache-Strategie. In diesem Artikel fasse ich Fallstricke, Messdaten und kopierfertige Konfigurationen zusammen. Wenn Sie auch auf CI warten, sparen Sie damit vermutlich viel Zeit.

1. Kernkonzepte des Cache-Mechanismus

Wer versteht, wie Cache arbeitet, konfiguriert ihn ohne böse Überraschungen.

GitHub Actions Cache folgt drei Schritten: Suchen → Wiederherstellen → Speichern. Sie definieren einen key; GitHub sucht einen passenden Cache. Gefunden wird ins Arbeitsverzeichnis restored; sonst wird nach dem Job ein neuer Cache gespeichert.

Wichtige Grenzen:

GrenzeWert
Cache-Obergrenze pro Repository10 GB
Maximale Größe einer Cache-Datei5 GB (über 1 GB wird es oft problematisch)
Aufbewahrung7 Tage ohne Zugriff → Löschung
Globale parallele Uploadsmaximal 5 gleichzeitig

Die 10-GB-Grenze kenne ich aus der Praxis: viele Abhängigkeiten, Cache wächst, neuer Cache passt nicht mehr, Altes wird entfernt – jeder Build startet „kalt“.

Cache und Artifact sind nicht dasselbe. Cache optimiert CI-Geschwindigkeit; Artifacts sind Build-Artefakte oder Reports für Menschen, oft länger haltbar. Cache: 10-GB-Limit; Artifacts: kein gleiches Limit, aber Repository-Speicher.

Docker Layer Cache ist ein eigener Mechanismus für Image-Builds – dazu später mehr.

2. Cache-Key-Design

Trefferquote hängt am key – das Herzstück der Strategie.

Was ist hashFiles()?

hashFiles() berechnet einen Hash – typisch für package-lock.json oder yarn.lock. Unveränderte Abhängigkeiten → gleicher Hash → Cache-Treffer.

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

Erzeugt z. B. npm-Linux-a1b2c3d4e5f6.... Ändert sich package-lock.json nicht, bleibt der key stabil.

restore-keys: Fallback

Bei Dependency-Updates helfen restore-keys als „abgestufte“ Suche:

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

Zuerst exakter key; sonst der neueste Cache mit Präfix npm-Linux-. Nicht vollständiger Treffer, aber viele Pakete in node_modules sind schon da – nur Delta-Installation nötig.

Drei Key-Muster im Vergleich

Aus Tests empfehle ich:

Einfach (kleine Projekte):

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

Mit Version (mehrere Node-Versionen):

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

Mehrere Pfade (Monorepo):

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

Treffer erkennen

actions/cache liefert 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 = exakter Treffer; false = Teil-Treffer oder Miss. Damit können Sie npm ci bedingt ausführen:

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

3. Praxis-Konfigurationen

Theorie reicht – hier getestete Snippets zum Kopieren.

npm (setup-node empfohlen)

setup-node hat eingebautes Caching – oft schlanker als manuelles actions/cache:

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

Eine Zeile. Für andere Verzeichnisse (z. B. node_modules) weiterhin actions/cache:

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

Empfehlung: Zuerst eingebautes Cache von setup-node, außer bei Sonderfällen.

yarn und pnpm

yarn nutzt andere Pfade:

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

pnpm mit globalem Store:

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

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

Python/pip

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

Docker Layer Cache

Image-Builds sind teuer; BuildKit unterstützt den GitHub Actions Cache-Backend:

- 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 speichert Layer im Actions-Cache. In Tests: 5-Minuten-Build oft unter 1 Minute.

Go-Module

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

Rust Cargo

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

Rust-Compile profitiert stark; target wächst jedoch – gelegentlich aufräumen.

4. Performance und Best Practices

Messdaten und typische Fehler aus der Praxis.

Benchmarks

Laut RunsOn-Report (Stand Januar 2026) mit sinnvollem Cache:

VorgangOhne CacheMit CacheFaktor
npm install3 Min.40 Sek.~5×
yarn install2 Min. 30 Sek.35 Sek.~4×
Docker build5 Min.1 Min.~5×
pip install45 Sek.8 Sek.~5×

Trefferquote typisch 70–90 % – abhängig vom Key-Design.

Häufige Fallen

Nicht direkt node_modules cachen

# So nicht
path: node_modules

node_modules ist plattformabhängig – Linux-Pakete können auf Windows scheitern. Besser globalen Cache (~/.npm) und npm ci lokal zusammensetzen.

Cross-OS: GNU tar + zstd

Standard-tar unterscheidet sich auf macOS/Windows – Restore kann scheitern:

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

Cache-Verunreinigung

Defekte Abhängigkeiten im Cache → wiederholte Build-Fehler:

  1. Manuell löschen: Repository → Actions → Caches
  2. Key erzwingen: Präfix oder Version im key
key: npm-v2-{{ runner.os }}-{{ hashFiles('**/package-lock.json') }}

Checkliste

Vor dem Rollout prüfen:

  1. Eingebautes Cache in offiziellen Actions (setup-node, setup-python)
  2. hashFiles im key, damit Dependency-Updates den Cache invalidieren
  3. restore-keys für Teil-Treffer
  4. Kein node_modules-Cache, globale Verzeichnisse nutzen
  5. Alte Caches löschen, unter 10 GB bleiben

5. Häufige Fragen

F1: Niedrige Trefferquote?

Oft wechselt der key zu oft – Zeitstempel oder Branch im key. Lösung: nur runner.os und hashFiles.

Oder hashFiles trifft zu viele Dateien, z. B. hashFiles('**/*.json') – jede Config-Änderung invalidiert. Nur package-lock.json / yarn.lock matchen.

F2: Cache-Speicher voll?

10 GB reichen nicht für große Monorepos oder Docker-Layer. Lösungen:

  1. Actions → Caches manuell aufräumen
  2. Getrennte keys pro Abhängigkeitstyp
  3. self-hosted runners ohne 10-GB-Limit

F3: self-hosted runners?

Gleiche Konfiguration. Vorteil: lokaler Cache, kein Netzwerk-Restore. Nachteil: keine Auto-Bereinigung – eigene Wartung.

F4: Cache erzwingen aktualisieren?

Key-Version erhöhen:

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

Oder alten Cache in der UI löschen.

Zusammenfassung

Kurz gesagt: gut konfigurierter Cache macht CI etwa fünfmal schneller.

Rechnen wir: 2 Minuten pro Build gespart, 10 Läufe täglich → 600 Minuten im Monat, rund 10 Stunden – Zeit für mehrere Artikel.

Einsteiger: mit setup-node und cache: 'npm' starten. Bei Engpässen Key-Strategie und Docker Layer Cache vertiefen.

Dies ist Teil 3 der Serie „GitHub Actions Praxis“. Zuvor: Pipeline-Aufbau und Deployment – in älteren Beiträgen nachlesen.

Beim nächsten Push die Build-Zeit prüfen: von 3 Minuten auf 40 Sekunden – ausprobieren lohnt sich.

GitHub Actions Cache für schnelleres CI/CD einrichten

Mit GitHub Actions Cache die npm-install-Zeit von 3 Minuten auf 40 Sekunden reduzieren

⏱️ Estimated time: 10 min

  1. 1

    Step 1: Cache-Strategie wählen

    Je nach Paketmanager:

    • npm: setup-node mit eingebautem Cache
    • yarn/pnpm: Cache-Pfade konfigurieren
    • Docker: BuildKit-Backend gha nutzen
  2. 2

    Step 2: Cache-Key entwerfen

    hashFiles() auf Basis der Lock-Datei:

    • Basis: {{ runner.os }}-node-{{ hashFiles('**/package-lock.json') }}
    • restore-keys als Fallback
    • Keine Zeitstempel oder Branch-Namen im key
  3. 3

    Step 3: Cache-Konfiguration ergänzen

    Im Workflow:

    • npm: actions/setup-node@v4, cache: 'npm'
    • Eigene Pfade: actions/cache@v4
    • Docker: cache-from und cache-to setzen
  4. 4

    Step 4: Cache-Wirkung prüfen

    Treffer prüfen:

    • Ausgabe cache-hit (true = exakter Treffer)
    • Build-Zeit vergleichen (4–5× schneller)
    • Actions → Caches: Eintrag vorhanden
  5. 5

    Step 5: Cache regelmäßig pflegen

    Probleme vermeiden:

    • Speicher beobachten (Limit 10 GB)
    • Alte Caches löschen
    • Bei Verunreinigung Key-Präfix ändern

FAQ

Warum liegt die Cache-Trefferquote nur bei 30 %?
Meist ein Key-Design-Problem. Prüfen Sie, ob der key häufig wechselnde Werte enthält (Zeitstempel, Branch-Name) – nur runner.os und hashFiles verwenden. Außerdem: hashFiles-Pfad muss exakt zur Lock-Datei passen, keine Wildcards über zu viele Dateien.
Was passiert bei mehr als 10 GB Cache?
GitHub räumt die ältesten Caches automatisch auf. Empfehlungen:

• Abhängigkeitstypen getrennt cachen (npm, Docker, pip mit eigenem key)
• Unter Actions → Caches manuell aufräumen
• Monorepos: Repositorys trennen oder self-hosted runners
Können Branches Cache teilen?
Standard: nur aktueller Branch und Default-Branch (main/master). Für branchesübergreifendes Teilen Branch-Namen aus dem key entfernen, nur Datei-Hash nutzen. restore-keys kann Caches anderer Branches matchen.
Was ist bei self-hosted runners anders?
Gleicher Mechanismus, zwei Unterschiede: Vorteil – Cache lokal, Restore ohne Netzwerklatenz; Nachteil – kein 10-GB-Limit, aber keine Auto-Bereinigung – eigene Skripte für alte Caches nötig.
Bricht ein fehlgeschlagenes Cache-Restore den Build ab?
Nein. Cache ist optionale Optimierung – bei Miss läuft der Build weiter, Abhängigkeiten werden neu geladen. Im Log: „Cache not found for key: xxx“, danach wird neuer Cache für den nächsten Lauf gespeichert.
Wann muss der Cache aktualisiert werden?
Drei Fälle:

• Abhängigkeitsversionen: hashFiles automatisch
• Cache-Verunreinigung: Build bricht plötzlich ab – alten Cache löschen
• Konfigurationsänderung: z. B. Node-Version – Versionsnummer im key

Bei korrekter Konfiguration meist kein manuelles Management nötig.

5 Min. Lesezeit · Veröffentlicht am: 7. Apr. 2026 · Aktualisiert am: 9. Juli 2026

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog