# 8.4. Caching

Morloc Manual > Managing Runs | https://morloc-project.github.io/docs/runs/caching.html | prev: https://morloc-project.github.io/docs/runs/run-directory.md | next: https://morloc-project.github.io/docs/runs/compression.md

Mark any labeled call site `cache: true` in the program YAML and its result is memoized to disk. The next call with equivalent inputs against unchanged source code is served from the cache rather than recomputed.

The freshness check is content-based, not time-based. Morloc has **no** notion of "the source file is newer than the cache entry, so re-run." Editing a comment in an unrelated function will not invalidate any cache. Copying the program to a new path will not either. Two builds on two machines that emit byte-identical pool sources share the same cache namespace. This is a deliberate departure from `make`, `snakemake`, `nextflow`, and similar tools that use mtime as a freshness signal and routinely re-run the world after a `git checkout` or a clock skew.

The build parameters passed with `-X` (see the build parameters section) also participate in the key, since they can change the compiled output without changing the pool source — switching a Futhark backend or adding a compiler flag is treated as a distinct build rather than a cache hit.

## 8.4.1. Declaring a cached call

```yaml
labeled-groups:
  expensive_step: { cache: true }
```

```morloc
foo xs = expensive_step@slowfn xs
```

Every call into `expensive_step@slowfn` is memoized under the `expensive_step` cache label. The same group config also controls per-step logging (`log: true`); the two flags are independent and may be combined.

## 8.4.2. What goes in the hash

A cached entry is keyed by:

```
call_key = xxh64(pool_source_fingerprint, midx, arg_content_hashes ...)
```

where:

-   **`pool_source_fingerprint`** is `xxh64` of the rendered pool source text, seed-chained over the contents of any files listed under `hash-include:` in the program YAML. Editing the body of the cached function, of any function it calls, of any imported module that compiled into the same pool, or of any declared external data file, all shift this fingerprint.
-   **`midx`** is the compiler-assigned manifold id, deterministic per build.
-   **`arg_content_hashes`** are content-aware hashes of each argument’s **value**, walked through its msgpack schema. Two structurally equal inputs hash the same regardless of how their packet stored the data (inline bytes, shared-memory pointer, or temp file), and pointer bits are never themselves hashed.

The freshness test is therefore: same code + same code dependencies + same input values → cache hit. Anything else → miss.

## 8.4.3. Storage layout

The cache lives under one of these directories, in resolution order:

| Source | Notes |
| --- | --- |
| `MORLOC_CACHE_BASE` env var | Explicit override. Useful for Docker bind mounts and shared filesystems where SLURM workers need access to the same cache. |
| `$XDG_CACHE_HOME/morloc/cache` | If `XDG_CACHE_HOME` is set. |
| `~/.cache/morloc/cache` | Default. |

Inside, two file types coexist:

```
 . ~/.cache/morloc/cache/
 │
 ├── expensive_step/
 │   ├── 3f4a91d62b08e7d2.packet
 │   └── b772aa1c0e4ef801.packet
 ├── another_label/
 │   └── ...
 └── data/
     ├── ef46db3751d8e999.dat
     └── ...
```

-   **`<label>/<call_key>.packet`** is a small morloc data packet (`PACKET_TYPE_DATA`, source=FILE, format=MSGPACK) that points to a dat file under `data/`. One entry per distinct cached call.
-   **`data/<data_hash>.dat`** holds the cached value as schema-agnostic msgpack bytes. The filename is `xxh64` of those bytes, so equal return values consolidate onto one on-disk copy regardless of which label cached them or which pool language produced them.

The content-addressed split gives natural deduplication: a Python pool and an R pool that both cache the same dataset (say, a genome pulled from an external database) write the data once. A source edit that produces the same return value writes a new `.packet` pointer but reuses the existing `.dat`.

## 8.4.4. Reading and writing

On lookup, the runtime reads the `.packet` file and returns its bytes; the language pool’s `get_value` follows the FILE-source pointer through to the dat file. The on-disk format is opaque to the language pool — it sees only bytes that round-trip through the standard packet API, so future format changes (inline-small optimization, compression, alternative backends) require no recompile of pool code.

On store, the runtime materializes the input packet to msgpack (dereferencing any shared-memory or file-backed payload), hashes those bytes to choose the dat filename, skips the write if an identical dat already exists, and finally writes the small pointer packet under the label dir. Both writes go through an atomic tmp  
rename, so a partial file is never observed by another reader and concurrent writers to the same key cannot corrupt the on-disk state.

A cached call whose foreign body raises is **not** memoized — the cache wrap only stores on the happy path. A re-run with the same inputs fails the same way rather than silently serving a stale entry.
