whycache
Docker tells you that the build cache broke. whycache tells you why, which file did it, and what it cost you.
That is a real run against psf/black. Every commit
costs an eleven-minute rebuild, because COPY . /src/ pulls in .git, which
changes on every commit, and the expensive apt install + pip install +
hatch build steps all sit below it.
Nobody on that project is doing anything wrong. It is simply invisible — Docker prints which steps re-ran, never why.
Install
pip install whycache
Or from source, until the first PyPI release lands:
git clone https://github.com/allaboutaryan/whycache && pip install ./whycache
No daemon, no account, no config, no network calls. One dependency-free CLI.
Use
whycache # build the current directory
whycache path/to/project # or somewhere else
whycache . -- --build-arg V=2 # anything after -- goes to docker build
Run it once to record a baseline, then run it again after you change something.
What it tells you
A file changed. Which one, and only the ones that instruction actually copies:
X Cache broke at build step 10: COPY src ./src
Reason - 1 file(s) changed in the build context:
src/sqlfluff/api/info.py
Cost - 7.5s of this 7.5s build.
A build arg changed. Docker substitutes args into the command before caching it, so the change is visible if you know where to look:
X Cache broke at step 2: RUN echo "building 3.0.0" > /version
Reason - the instruction itself changed since the last build:
was: RUN echo "building 2.0.0" > /version
now: RUN echo "building 3.0.0" > /version
Junk is polluting your build context. With a concrete fix:
Fix - add to .dockerignore:
.git/ (changes on every commit, 2 file(s))
node_modules/ (reinstallable from lockfile, 41 file(s))
Saves ~4m12s per build.
...unless that fix would break your build. black derives its version from
git via hatch-vcs, so ignoring .git/ does not make the build slower — it
makes it fail:
.git/ is breaking the cache (1 file(s)), but do NOT ignore it:
pyproject.toml reads git metadata at build time, so excluding
.git/ makes the build fail rather than just slow.
This one is not hypothetical. An earlier version of this tool gave that advice,
and broke black's build with it.
The number tells you if your Dockerfile is well built
Same tool, three real projects:
| Project | What changed | Cache broke at | Cost |
|---|---|---|---|
| psf/black | one commit | COPY . /src/ (step 3 of 6) |
11m00s |
| sqlfluff | one source file | COPY src ./src (step 10 of 12) |
7.5s |
| traefik/whoami | go.mod |
COPY go.mod . (step 4) |
30.2s |
| traefik/whoami | app.go |
COPY . . (step 7) |
29.6s |
A miss high in the Dockerfile is expensive because everything below it re-runs. A miss near the bottom is cheap. That gap is the entire skill of writing a Dockerfile, and this is what makes it visible.
How it works
- Runs your build with
--progress=rawjsonand reads the structured output. - Finds the first step that was not
CACHED. - Fingerprints your build context, honouring
.dockerignore, and diffs it against the last run. - Sums the time of every step that re-ran.
State lives in ~/.whycache/, keyed by project path. Nothing is written into
your build context — doing that would change the context and break the cache,
which is the exact problem this tool exists to report.
What it will not do
- It will not edit your Dockerfile. It tells you; you decide.
- It will not guess. If the cache was cold, or the cause is outside the build context, it says so and names no file. A confidently wrong answer is worse than no answer, so it declines rather than blames.
- No AI. This is a deterministic diff of two file lists. An LLM would make it slower, costlier, and less trustworthy.
How it compares
| Tool | Answers |
|---|---|
dive |
How much space is wasted in the image |
hadolint |
Does the Dockerfile follow style rules |
whycache |
Why the cache missed on this build, and what it cost |
Different questions. dive and hadolint are both excellent; neither answers this one.
Requirements
- Docker with BuildKit (the default since Docker 23)
- Python 3.10+
Works on Linux, macOS, and Windows. It is developed on Windows, so the console output degrades to ASCII rather than crashing on cp1252 — a courtesy most Linux-first tooling forgets.
Overhead
Fingerprinting a 5,955-file / 25 MB context takes ~730 ms on a warm run, because unchanged files are never reopened. First run on a project costs ~6 s.
Contributing
Bug reports with a Dockerfile that reproduces are the most useful thing you can
send. The test suite runs without Docker except for test_e2e.py:
python test_dockerignore.py # .dockerignore matching
python test_whycache.py # step parsing, blame, git-versioning guard
python -m whycache.manifest # context fingerprinting
python test_e2e.py # full run against a real build (needs Docker)
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file whycache-0.1.0.tar.gz.
File metadata
- Download URL: whycache-0.1.0.tar.gz
- Upload date:
- Size: 19.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9c55a0dcd63ad9a734e5647d703097053c812972b418af74021a279b01c8bb04
|
|
| MD5 |
cd5b6833e97d4322d7ffffb449b5250a
|
|
| BLAKE2b-256 |
f9dc2aa19ae2d8ab8fda32b86ef4f462bb4a1a68aa1ae777c2dda18e99e41aed
|
File details
Details for the file whycache-0.1.0-py3-none-any.whl.
File metadata
- Download URL: whycache-0.1.0-py3-none-any.whl
- Upload date:
- Size: 14.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8de393e46ccc5c9ad29d3de928c19dddb06a29681253f4a4807ea21253129cc
|
|
| MD5 |
2d042f5d0e00a6be6e8461209178a102
|
|
| BLAKE2b-256 |
37ba8f9092971528bf1f77fd5b4e727d129a9be6f5def3cb9ef82ed66f6ff09e
|