Cache
Caching lets Loom jobs skip redundant work by saving and restoring outputs keyed on your inputs. When the inputs haven't changed, the cached outputs are restored instead of recomputing them — turning multi-minute dependency installs or build steps into seconds.
This page explains how caching works, how keys are resolved, what quarantine means, and how to design keys that are safe and effective.
Why caching matters
Every CI/CD run that re-downloads dependencies, re-compiles artifacts, or re-generates unchanged outputs wastes time and compute. Caching addresses this directly:
- Faster feedback loops — cache hits restore outputs in seconds instead of minutes.
- Lower resource usage — avoid redundant network fetches, disk I/O, and CPU cycles.
- Deterministic verification —
--cache-diffmode lets you trust caches while still proving correctness.
How it works
When a job with cache: configuration runs:
- Key resolution — Loom computes a cache key from the
key:configuration (prefix + file hashes, or a template string). - Quarantine check — if the resolved key is quarantined, Loom skips the restore and runs the job fresh.
- Restore attempt — Loom looks up the key in the cache store. On a hit, cached paths are extracted into the workspace. On a miss, the job runs without cached outputs.
- Job execution — the job's
script:commands run. - Save — after execution, Loom archives the declared
paths:and writes them to the cache store under the resolved key.
The policy: and when: fields control which of these steps happen (see Configuration reference below).
Configuration reference
Cache is configured per-job in your workflow YAML. It supports two forms: a mapping (single cache) and a sequence (multiple named caches).
Cache fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Only in sequence form | Unique identifier for this cache entry |
disabled | boolean | No | Set true to skip this cache entry |
paths | string[] | Yes (unless disabled) | Workspace-relative paths to cache (files or directories) |
key | string or mapping | No | How to compute the cache key (see Key resolution) |
fallback_keys | string[] | No | Additional keys to try on restore if the primary key misses |
policy | string | No | One of pull, push, pull-push (default: pull-push) |
when | string | No | When to save: on_success, on_failure, always (default: on_success) |
Policy values
| Policy | Restore on hit? | Save after job? | Use case |
|---|---|---|---|
pull-push | Yes | Yes | Default — full caching behavior |
pull | Yes | No | Read-only — consume caches without updating them |
push | No | Yes | Write-only — populate caches for other jobs/runs |
When values
| When | Saves on success? | Saves on failure? | Use case |
|---|---|---|---|
on_success | Yes | No | Default — only cache known-good outputs |
on_failure | No | Yes | Cache partial outputs for faster retry after failures |
always | Yes | Yes | Always save regardless of outcome |
Key resolution
Cache keys determine whether a restore is a hit or miss. Loom supports two key formats:
Prefix + files (recommended)
The most common form. Loom hashes the contents of the specified files and appends the digest to the prefix:
cache:
key:
prefix: my-project-deps
files: [pnpm-lock.yaml]
paths: [.pnpm-store, node_modules]
The resolved key becomes my-project-deps-<sha256>, where the SHA-256 digest is computed from the sorted file contents. If a listed file doesn't exist, Loom contributes a MISSING:<path> sentinel to the hash — so the key still changes when files appear or disappear.
Glob patterns are supported in files: (e.g., **/go.sum).
Template string
For full control, use a template string with runtime variables:
cache:
key: "deps-${job_name}-${head_sha}"
paths: [.pnpm-store]
Available template variables:
| Variable | Description |
|---|---|
$job_name | Job name (graph node ID) |
$job_id | Job identifier |
$run_id | Current run ID |
$pipeline_id | Pipeline ID |
$head_sha | HEAD commit SHA of the snapshot |
Fallback keys
When the primary key misses, Loom tries each fallback_keys entry in order. This is useful for partial cache hits — for example, restoring from a previous lockfile version:
cache:
key:
prefix: deps
files: [pnpm-lock.yaml]
fallback_keys:
- "deps-${job_name}"
paths: [.pnpm-store]
Cache storage format
Cached outputs are stored as zstd-compressed tar archives (.tar.zst). Each cache entry produces two files in the cache store:
<cache_root>/
<scope_hash>/
<key_hash>.tar.zst # archived paths
<key_hash>.json # manifest (key, scope, paths, timestamp)
The scope_hash isolates caches by scope (preventing cross-project collisions), and key_hash is the SHA-256 of the resolved key string.
Concrete example
Single cache (mapping form)
version: v1
stages: [ci]
unit:
stage: ci
target: linux
cache:
key:
prefix: loom-cache-unit
files: [pnpm-lock.yaml]
paths: [.pnpm-store, .nx/cache]
policy: pull-push
when: always
script:
- pnpm i --frozen-lockfile
- pnpm test
Multiple caches (sequence form)
When a job needs separate caches with independent lifecycles, use the sequence form. Each entry requires a unique name:
version: v1
stages: [ci]
build:
stage: ci
target: linux
cache:
- name: deps
key:
prefix: deps
files: [pnpm-lock.yaml]
paths: [.pnpm-store, node_modules]
policy: pull-push
- name: build-cache
key:
prefix: build
files: [pnpm-lock.yaml, tsconfig.base.json]
paths: [.nx/cache, dist]
policy: pull-push
when: on_success
script:
- pnpm i --frozen-lockfile
- pnpm build
Disabling cache
Set cache: null or cache: [] to explicitly disable caching for a job. In sequence form, set disabled: true on individual entries:
cache:
- name: deps
disabled: true
paths: [.pnpm-store]
Trust-but-verify with --cache-diff
Run with --cache-diff to verify that cached outputs match what the job would produce from scratch:
loom run --local --cache-diff --workflow .loom/workflow.yml
In this mode, Loom restores cached outputs and runs the job, then compares the results. If they diverge, the cache entry is quarantined.
Use --cache-diff when:
- Introducing caching for the first time
- Changing cache keys or paths
- Investigating "works locally but fails in CI" issues related to stale artifacts
Quarantine
When --cache-diff detects that cached outputs diverge from freshly produced outputs, Loom quarantines the cache entry. Quarantined entries are skipped on future restore attempts until the underlying issue is resolved.
Where quarantine state lives
Quarantine entries are stored in a single JSON file at the cache store root:
<cache_root>/quarantine.json
The file contains an array of quarantine entries:
{
"schema_version": "v1",
"entries": [
{
"scope_hash": "abc123...",
"key_hash": "def456...",
"key": "deps-loom-cache-unit-<sha256>",
"reason": "cache_diff_divergence",
"quarantined_at": "2026-03-04T12:00:00.000Z",
"evidence": {}
}
]
}
How to recover from quarantine
- Confirm the divergence is real — check whether the outputs are genuinely nondeterministic (timestamps, randomness) or whether inputs changed without the key reflecting it.
- Fix the key — update
key.filesto include the inputs that actually changed, so the cache key changes when outputs should change. - Remove the quarantine entry — delete the relevant entry from
quarantine.json, or delete the file entirely to clear all quarantines.
Cache key design checklist
Use this checklist whenever you add or review cache configuration:
- Include inputs that change outputs: lockfiles, build configs, compiler flags, tool versions
- Scope keys to avoid collisions: use meaningful prefixes that include project/job context
- Cache only the right paths: include produced artifacts, exclude transient/temp files
- Avoid caching directories with absolute paths: paths that embed machine-specific state cause false hits
- Test with
--cache-diff: verify determinism before trusting cache hits
When not to cache
Avoid caching jobs or steps that are inherently nondeterministic or where reuse creates risk:
| Situation | Why caching is risky |
|---|---|
| Nondeterministic outputs | Timestamps, randomness, or "latest" tags produce different outputs from the same inputs |
| Secret-bearing outputs | Artifacts that could contain credentials, even transiently |
| External-state dependent steps | Queries against mutable services where outputs change without input changes |
| Debug or diagnostic steps | Where you need fresh logs/output every time |
If you're unsure, start with --cache-diff until you've proven determinism.
Common pitfalls
| Pitfall | Fix |
|---|---|
| Treating cache hits as proof of correctness | Use --cache-diff to verify |
| Cache key doesn't include all relevant inputs | Add missing files/configs to key.files |
| Caching paths that embed absolute paths | Use relative, machine-independent paths |
Overly broad paths: that include transient files | Narrow paths: to only the outputs you need |
| Not scoping keys by project or job | Add meaningful prefixes to avoid cross-job collisions |
What to read next
- Syntax v1 — cache configuration
- Providers — how execution providers interact with cache storage
- Isolated workspace — the snapshot context where cached paths are restored
- Receipts — cache evidence is included in run receipts
- CLI
loom run—--cache-diffflag reference