Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXLVI · CachingCaching mechanisms

Cache keys and restore keys — exact match, prefix fallback, partial reuse

Intermediate⏱ ~20 mingit

What you'll learn

  • Distinguish exact-match keys from prefix-match restore-keys
  • Apply ordered restore-keys to support a Python-version matrix with shared fallback
  • Recognise the partial-reuse pattern: restore a near-match cache and let the install step top it up
  • Identify the failure mode of a too-broad restore-keys prefix (cross-ecosystem collision)

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

The exact-match key and the prefix-match restore-keys are two different lookup mechanisms the cache step uses in sequence. The exact-match key is the identity of the cache entry: the runner looks it up, and on a hit, restores the path. The restore-keys are an ordered list of fallbacks: on a miss, the runner tries each restore-key as a prefix against the cache store and restores the most recent entry that matches. Used together, the two mechanisms let a workflow reuse a near-match entry when the lockfile has changed slightly - a useful pattern that is also a source of subtle misconfigurations.

Exact match and prefix match

The two lookup mechanisms have different semantics:

  • Exact match (key) returns the entry whose key equals key. There is at most one such entry on a given branch. A hit restores the path; a miss proceeds to fallback.
  • Prefix match (restore-keys) returns the most recent entry whose key starts with the restore-key string. The lookup is ordered: the runner tries each restore-key in the order it appears, returning the first match. The result of a prefix match is “the entry I would have wanted if I had been less specific”.
flowchart TB
    A["Exact key: pip-<sha-A>"] --> B{"Exact hit?"}
    B -->|yes| C["Restore exact entry"]
    B -->|no| D["Try restore-keys in order"]
    D --> E["restore-key 1: pip-py3.12-"]
    E --> F{"Prefix hit?"}
    F -->|yes| G["Restore most-recent prefix match"]
    F -->|no| H["restore-key 2: pip-"]
    H --> I{"Prefix hit?"}
    I -->|yes| J["Restore most-recent prefix match"]
    I -->|no| K["Complete miss"]
    K --> L["Run install from scratch"]
    C --> M["Job runs"]
    G --> M
    J --> M
    L --> M

The diagram shows the resolution order. The exact key is tried first. The restore-keys are tried in declaration order. The first match wins. A complete miss falls through to the install step.

Partial reuse: the canonical use case

The canonical use case for restore-keys is partial reuse. A workflow supports Python 3.11 and 3.12; the lockfile changes slightly between the two; the runner wants to reuse the 3.11 cache when the 3.12 cache misses.

- name: Cache pip
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: pip-${ matrix.python-version }-${ hashFiles('requirements.txt') }
    restore-keys: |
      pip-${ matrix.python-version }-
      pip-

The exact key is pip-py3.12-<sha-of-requirements.txt>. On a miss, the runner tries pip-py3.12- first (the most recent 3.12-cached entry) and then pip- (any recent pip entry). The partial-reuse pattern works because pip’s wheel cache is largely compatible across patch versions of the same Python version: a wheel built for Python 3.12.0 is reusable for Python 3.12.1. The install step tops up the cache with whatever new wheels the new lockfile requires.

The cost of partial reuse is that the cache is not exact: the restored entry may contain wheels that the new lockfile does not need. The wheel cache is large but eviction-tolerant - the unneeded wheels are simply not used and the cache store evicts them under LRU pressure. The benefit is that the install step downloads only the new wheels, not the entire dependency tree.

The cross-ecosystem collision failure mode

A restore-keys prefix that is too broad causes a different cache entry to be restored - one that belongs to a different ecosystem.

# WRONG: prefix is just '-'
- name: Cache pip
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: pip-${ hashFiles('requirements.txt') }
    restore-keys: |
      -

The restore-keys prefix - matches any cache key with a hyphen anywhere in it: pip-&lt;sha&gt;, npm-&lt;sha&gt;, maven-&lt;sha&gt;, go-&lt;sha&gt;. The runner restores the most recent matching entry, regardless of which ecosystem created it. The pip cache is then populated with npm or maven entries, and the install step fails because the entries are not pip wheels.

The fix is a prefix that includes the ecosystem namespace:

# RIGHT: prefix is the ecosystem namespace
- name: Cache pip
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: pip-${ hashFiles('requirements.txt') }
    restore-keys: |
      pip-

The prefix pip- matches only pip entries. The cross-ecosystem collision is impossible. The cost is that a near-match across ecosystems is no longer possible - but that is correct, because the wheel formats are incompatible.

Multi-line restore-keys and partial reuse

Multi-line restore-keys are not just for cross-version fallback. They are useful whenever the cache entry may have changed slightly but a near-match is still useful:

restore-keys: |
  npm-node18-
  npm-node20-
  npm-

The runner tries npm-node18- first (specific to Node 18 on this branch), then npm-node20- (Node 20), then npm- (any recent npm cache). The most specific match wins. The install step tops up with whatever the new lockfile needs.

The order of restore-keys is also the order of cost: the most specific prefix is the most likely match but the smallest scope; the broadest prefix is the largest scope but the highest risk of cross-ecosystem collision. Listing the prefixes most-specific first is the right trade-off.

When restore-keys hurts

Restore-keys are not always an improvement. Three situations make them counterproductive:

  • The lockfile changes are large. A workflow that bumps major-version dependencies on every release gets little benefit from a prefix-match fallback: the restored wheels are mostly wrong-version and the install step still has to download most of the dependency tree.
  • The cache contents are not cross-version compatible. Native binaries (compiled C extensions, kernel modules) are not compatible across major Python versions. The prefix-match restores an entry that the install step cannot use.
  • The path includes build artefacts that are not cross-version compatible. A target/ directory in a Rust build is keyed by the Rust toolchain version; restoring a near-match forces a full rebuild anyway.

In all three cases, the right answer is to drop restore-keys and let the install step do the work.

Production discipline

  1. Use ordered, ecosystem-namespaced restore-keys. The first entry is the most specific prefix; the last entry is the broadest acceptable prefix. Never use a single-character or punctuation-only prefix.
  2. Test the prefix match. After changing the lockfile, verify that the restore step reports a prefix-match hit (the cache step’s success message indicates whether the hit was exact or prefix).
  3. Drop restore-keys when the cache contents are not cross-version compatible. A too-clever restore-keys configuration that forces a full rebuild on every run is worse than no restore-keys at all.
  4. Document the order of restore-keys. The maintainer who reads the workflow six months later should not have to reverse-engineer why a particular prefix was listed first.

Cross-course references

  • Linux for Production Sysadmins - Part XXXIV (ConfigMgmt) applies the same ordered-prefix pattern to apt package caches, where restore-keys: apt-bookworm- precedes apt-.
  • Ansible for Production Sysadmins - Part XXXVII (RepoArch) applies the pattern to molecule cache, where the molecule version is the prefix namespace.
  • Terraform for Production Sysadmins - Parts IX-XII (State) use restore-keys for the Terraform provider plugin cache, keyed by the provider version prefix.

Quiz

Knowledge check · 4 questions

  1. Q1. A workflow lists `restore-keys: |` with `npm-node18-` first and `npm-` second. After a Node version bump to Node 20, what happens?

  2. Q2. A restore-keys prefix of `-` (a single hyphen) is acceptable as a generic fallback because it matches any cache key.

  3. Q3. Explain why the order of restore-keys matters and what failure mode follows from reversing it.

  4. Q4. Redesign a cache configuration where a too-broad restore-keys prefix causes a cross-ecosystem collision so that pip, npm, and maven caches coexist in the same repository without interference.

    Team D's repository has three workflows: a Python workflow with `key: pip-${{ hashFiles(...) }}` and `restore-keys: |` with content `-`; an npm workflow with `key: npm-${ hashFiles(...) }` and the same `-` fallback; a maven workflow with `key: maven-${ hashFiles(...) }` and the same fallback. Once a week, the Python workflow restores an npm cache entry under `~/.cache/pip`, and the install step fails with 'no such file or directory' for the npm-specific wheel paths.

Passing score: 75%. Answers are checked in this browser.