Skip to content

Commit 02e19a9

Browse files
authored
Merge pull request #10398 from circleci/update-caching-strategies
Updating Caching Strategies for Node and Python
2 parents 741bf49 + 8057e00 commit 02e19a9

1 file changed

Lines changed: 102 additions & 19 deletions

File tree

docs/guides/modules/optimize/pages/caching-strategy.adoc

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Caching is one of the most effective ways to make jobs faster on CircleCI. By re
88
[#caching-and-self-hosted-runner]
99
== Cache storage customization
1010

11-
When using self-hosted runners, there is a network and storage usage limit included in your plan. Some actions related to artifacts will accrue network and storage usage. Once your usage exceeds your limit, charges will apply.
11+
When using self-hosted runners, there is a network and storage usage limit included in your plan. Some actions related to artifacts accrue network and storage usage. Once your usage exceeds your limit, charges apply.
1212

1313
Retaining caches for a long period of time will have storage cost implications. It is best to determine why you are retaining caches, and how long caches need to be retained for your use case. To lower costs, consider a lower storage retention for caches, if that suits your needs.
1414

@@ -27,14 +27,14 @@ Using cache keys that are too strict can mean that you will get a minimal number
2727
[#avoid-unnecessary-workflow-reruns]
2828
=== Avoid unnecessary workflow reruns
2929

30-
If your project has "flaky tests," workflows might be rerun unnecessarily. This will both use up your credits and increase your storage usage. To avoid this situation, address flaky tests. For help with identifying them, see xref:insights:insights-tests.adoc#flaky-tests[Test Insights].
30+
If your project has "flaky tests," workflows are rerun unnecessarily. This uses up your credits and increases your storage usage. To avoid this situation, address flaky tests. For help with identifying them, see xref:insights:insights-tests.adoc#flaky-tests[Test Insights].
3131

3232
You can also consider configuring your projects to rerun failed jobs rather than entire workflows. To achieve this you can use the `when` step. For further information, see the xref:reference:ROOT:configuration-reference.adoc#the-when-attribute[Configuration Reference].
3333

3434
[#split-cache-keys-by-directory]
3535
=== Split cache keys by directory
3636

37-
Having multiple directories under a single cache key increases the chance of there being a change to the cache. In the example below, there may be changes in the first two directories but no changes in the `a` or `b` directory. Saving all four directories under one cache key increases the potential storage usage. The cache restore step will also take longer than needed as all four sets of files will be restored.
37+
Having multiple directories under a single cache key increases the chance of there being a change to the cache. In the example below, there may be changes in the first two directories but no changes in the `a` or `b` directory. Saving all four directories under one cache key increases the potential storage usage. The cache restore step also takes longer than needed as all four sets of files are restored.
3838

3939
[,yaml]
4040
----
@@ -95,7 +95,7 @@ If you notice your cache usage is high and would like to reduce it:
9595
9696
* Search for the `save_cache` and `restore_cache` commands in your `.circleci/config.yml` file to find all jobs utilizing caching and determine if their cache(s) need pruning.
9797
* Narrow the scope of a cache from a large directory to a smaller subset of specific files.
98-
* Ensure that your cache `key` is following xref:caching.adoc#further-notes-on-using-keys-and-templates[best practices]:
98+
* Ensure that your cache `key` is following xref:caching.adoc#further-notes-on-using-keys-and-templates[Best Practices]:
9999
+
100100
[,yaml]
101101
----
@@ -106,12 +106,12 @@ If you notice your cache usage is high and would like to reduce it:
106106
- /usr/local/Homebrew
107107
----
108108
+
109-
Notice in the above example that best practices are not followed. `brew-{{ epoch }}` will change every build causing an upload every time even if the value has not changed. This will cost you money, and never save you any time. Instead pick a cache `key` like the following:
109+
Notice in the above example that best practices are not followed. `brew-{{ epoch }}` changes every build, causing an upload every time even if the value has not changed. This costs you money, and never saves you any time. Instead pick a cache `key` like the following:
110110
+
111111
[,yaml]
112112
----
113113
- save_cache:
114-
key: brew-{{checksum Brewfile}}
114+
key: brew-{{checksum "Brewfile"}}
115115
paths:
116116
- /Users/distiller/Library/Caches/Homebrew
117117
- /usr/local/Homebrew
@@ -136,7 +136,7 @@ Some dependency managers do not properly handle installing on top of partially r
136136
- gem-cache
137137
----
138138
139-
In the above example, if a dependency tree is partially restored by the second or third cache keys, some dependency managers will incorrectly install on top of the outdated dependency tree.
139+
In the above example, the second or third cache keys may produce a partial restore. Some dependency managers then incorrectly install on top of the outdated dependency tree.
140140
141141
Instead of a cascading fallback, a more stable option is a single version-prefixed cache key:
142142
@@ -237,7 +237,7 @@ Since Leiningen uses Maven under the hood, it behaves in a similar way.
237237
*Safe to Use Partial Cache Restoration?*
238238
Yes (with NPM5+).
239239
240-
With NPM5+ and a lock file, you can safely use partial cache restoration.
240+
With NPM5+ and a lock file, you can use partial cache restoration. Cache the npm download cache at `~/.npm` rather than `node_modules` directly. The `~/.npm` path is the correct cache location regardless of npm version, and works with both `npm install` and `npm ci`.
241241
242242
[,yaml]
243243
----
@@ -248,19 +248,55 @@ With NPM5+ and a lock file, you can safely use partial cache restoration.
248248
- node-v1-{{ .Branch }}-{{ checksum "package-lock.json" }}
249249
- node-v1-{{ .Branch }}-
250250
- node-v1-
251+
- run: npm install
251252
- save_cache:
252253
paths:
253-
- ~/usr/local/lib/node_modules # location depends on npm version
254+
- ~/.npm
254255
key: node-v1-{{ .Branch }}-{{ checksum "package-lock.json" }}
255256
----
256257
258+
[NOTE]
259+
====
260+
If your job uses `npm ci` rather than `npm install`, be aware that `npm ci` deletes and rebuilds `node_modules` on every run by design. Caching `~/.npm` (the download cache) still provides a benefit because it avoids re-downloading packages from the registry, but the time saving is smaller than with `npm install`. If you find your `restore_cache` step is adding more time than it saves when using `npm ci`, consider switching to `npm install` with a lock file, or removing the cache step entirely.
261+
====
262+
257263
[#pip-python]
258264
=== `pip` (Python)
259265
260266
*Safe to Use Partial Cache Restoration?*
261-
Yes (with Pipenv).
267+
Depends on what you cache.
268+
269+
The safety of partial cache restoration for Python depends on whether you cache the *download cache* or the *installed environment*:
270+
271+
* *Download cache* (`~/.cache/pip`): pip stores downloaded wheels here before installing them. Partially restoring this cache is safe because pip always resolves and installs the correct versions from your dependency file regardless of what wheels are already present. A stale partial restore causes some wheels to be re-downloaded.
272+
* *Installed environment* (a `virtualenv` or `site-packages` directory): partially restoring an installed environment is unsafe unless you use a lock file that pins every dependency to an exact version. Without pinned versions, pip may leave stale package versions in place rather than upgrading them, producing an environment that does not match a clean install.
273+
274+
The examples below follow this distinction. Bare pip and `uv` cache the download cache and use fallback keys without risk. Pipenv and Poetry cache the installed environment and rely on their lock files to keep partial restores deterministic.
275+
276+
[#pip-requirements-txt]
277+
==== Pip with requirements.txt
278+
279+
Cache the pip download cache at `~/.cache/pip`. Partial restoration is safe here because pip resolves installs from `requirements.txt` independently of what is already cached.
280+
281+
[,yaml]
282+
----
283+
steps:
284+
- restore_cache:
285+
keys:
286+
- pip-cache-v1-{{ .Branch }}-{{ checksum "requirements.txt" }}
287+
- pip-cache-v1-{{ .Branch }}-
288+
- pip-cache-v1-
289+
- run: pip install -r requirements.txt
290+
- save_cache:
291+
paths:
292+
- ~/.cache/pip
293+
key: pip-cache-v1-{{ .Branch }}-{{ checksum "requirements.txt" }}
294+
----
262295
263-
Pip can use files that are not explicitly specified in `requirements.txt`. Using link:https://docs.pipenv.org/[Pipenv] will include explicit versioning in a lock file.
296+
[#pip-pipenv]
297+
==== Pipenv
298+
299+
Pipenv generates a `Pipfile.lock` that pins every dependency to an exact version. This makes partial restoration of the installed `virtualenv` safe, because the lock file ensures pip installs the correct versions even on top of a stale partial restore.
264300
265301
[,yaml]
266302
----
@@ -271,12 +307,58 @@ Pip can use files that are not explicitly specified in `requirements.txt`. Using
271307
- pip-packages-v1-{{ .Branch }}-{{ checksum "Pipfile.lock" }}
272308
- pip-packages-v1-{{ .Branch }}-
273309
- pip-packages-v1-
310+
- run: pipenv install
274311
- save_cache:
275312
paths:
276313
- ~/.local/share/virtualenvs/venv # this path depends on where pipenv creates a virtualenv
277314
key: pip-packages-v1-{{ .Branch }}-{{ checksum "Pipfile.lock" }}
278315
----
279316
317+
[#pip-poetry]
318+
==== Poetry
319+
320+
link:https://python-poetry.org/[Poetry] generates a `poetry.lock` file that pins all dependencies to specific versions, making it a reliable cache key. As with Pipenv, the lock file makes partial restoration of the installed `virtualenvs` directory safe.
321+
322+
[,yaml]
323+
----
324+
steps:
325+
- restore_cache:
326+
keys:
327+
- poetry-v1-{{ .Branch }}-{{ checksum "poetry.lock" }}
328+
- poetry-v1-{{ .Branch }}-
329+
- poetry-v1-
330+
- run: poetry install --no-interaction
331+
- save_cache:
332+
paths:
333+
- ~/.cache/pypoetry/virtualenvs
334+
key: poetry-v1-{{ .Branch }}-{{ checksum "poetry.lock" }}
335+
----
336+
337+
[#pip-uv]
338+
==== `uv`
339+
340+
link:https://docs.astral.sh/uv/[`uv`] is a fast Python package installer that maintains its own download cache at `~/.cache/uv`. Like bare pip, this is a download cache rather than an installed environment, so partial restoration is safe.
341+
342+
[,yaml]
343+
----
344+
steps:
345+
- restore_cache:
346+
keys:
347+
- uv-cache-v1-{{ .Branch }}-{{ checksum "uv.lock" }}
348+
- uv-cache-v1-{{ .Branch }}-
349+
- uv-cache-v1-
350+
- run: uv sync --frozen
351+
- save_cache:
352+
paths:
353+
- ~/.cache/uv
354+
key: uv-cache-v1-{{ .Branch }}-{{ checksum "uv.lock" }}
355+
----
356+
357+
[NOTE]
358+
====
359+
`uv` installs packages faster than pip, so the time saving from caching may be smaller than with other tools. Whether caching is worthwhile depends on the size of your dependency tree and how frequently it changes.
360+
====
361+
280362
[#yarn-node]
281363
=== Yarn (Node)
282364
@@ -294,6 +376,7 @@ Yarn has always used a lock file for the reasons explained above.
294376
- yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
295377
- yarn-packages-v1-{{ .Branch }}-
296378
- yarn-packages-v1-
379+
- run: yarn --frozen-lockfile --cache-folder ~/.cache/yarn
297380
- save_cache:
298381
paths:
299382
- ~/.cache/yarn
@@ -302,7 +385,7 @@ Yarn has always used a lock file for the reasons explained above.
302385
303386
We recommend using `yarn --frozen-lockfile --cache-folder ~/.cache/yarn` for two reasons:
304387
305-
* `--frozen-lockfile` ensures a whole new lockfile is created and it also ensures your lockfile is not altered. This allows for the checksum to stay relevant and your dependencies should identically match what you use in development.
388+
* `--frozen-lockfile` ensures a whole new lockfile is created and it also ensures your lockfile is not altered. This allows for the checksum to stay relevant and your dependencies will identically match what you use in development.
306389
307390
* The default cache location depends on OS. `--cache-folder ~/.cache/yarn` ensures you are explicitly matching your cache save location.
308391
@@ -313,18 +396,18 @@ We recommend using `yarn --frozen-lockfile --cache-folder ~/.cache/yarn` for two
313396
314397
In cases where the build tools for your language include elegant handling of dependencies, partial cache restores may be preferable to zero cache restores for performance reasons. If you get a zero cache restore, you have to reinstall all your dependencies, which can cause reduced performance. One alternative is to get a large percentage of your dependencies from an older cache, instead of starting from zero.
315398
316-
However, for other language types, partial caches carry the risk of creating code dependencies that are not aligned with your declared dependencies and do not break until you run a build without a cache. If the dependencies change infrequently, consider listing the zero cache restore key first, and then track the costs over time.
399+
However, for other language types, partial caches carry the risk of creating code dependencies that are not aligned with your declared dependencies. These mismatches do not always surface until you run a build without a cache. If the dependencies change infrequently, consider listing the zero cache restore key first, and then track the costs over time.
317400
318401
If the performance costs of zero cache restores (also referred to as a _cache miss_) prove significant over time, only then consider adding a partial cache restore key.
319402
320-
Listing multiple keys for restoring a cache increases the chances of a partial cache hit. However, broadening your `restore_cache` scope to a wider history increases the risk of confusing failures. For example, if you have dependencies for Node v6 on an upgrade branch, but your other branches are still on Node v5, a `restore_cache` step that searches other branches might restore incompatible dependencies.
403+
Listing multiple keys for restoring a cache increases the chances of a partial cache hit. However, broadening your `restore_cache` scope to a wider history increases the risk of confusing failures. For example, consider a project with Node v6 on an upgrade branch and Node v5 on all other branches. A `restore_cache` step that searches other branches may restore incompatible dependencies.
321404
322405
[#using-a-lock-file]
323406
== Using a lock file
324407
325408
Language dependency manager lockfiles (for example, `Gemfile.lock` or `yarn.lock`) checksums may be a useful cache key.
326409
327-
An alternative is to run the command `ls -laR your-deps-dir > deps_checksum` and reference it with `{{ checksum "deps_checksum" }}`. For example, in Python, to get a more specific cache than the checksum of your `requirements.txt` file, you could install the dependencies within a `virtualenv` in the project root `venv` and then run the command `ls -laR venv > python_deps_checksum`.
410+
An alternative is to run the command `ls -laR your-deps-dir > deps_checksum` and reference it with `{{ checksum "deps_checksum" }}`. For example, in Python, install the dependencies within a `virtualenv` in the project root `venv`. Then run `ls -laR venv > python_deps_checksum` to produce a more specific cache key than the checksum of your `requirements.txt` file.
328411
329412
[#using-multiple-caches-for-different-languages]
330413
== Using multiple caches for different languages
@@ -333,14 +416,14 @@ It is also possible to lower the cost of a cache miss by splitting your job acro
333416
334417
Consider splitting caches by language type (`npm`, `pip`, or `bundler`), if you know the following:
335418
336-
* How each dependency manager stores its files
337-
* How it upgrades
338-
* How it checks dependencies
419+
* How each dependency manager stores its files.
420+
* How it upgrades.
421+
* How it checks dependencies.
339422
340423
[#caching-expensive-steps]
341424
== Caching expensive steps
342425
343-
Certain languages and frameworks include more expensive steps that can and should be cached. Scala and Elixir are two examples where caching the compilation steps will be especially effective. Rails developers could also notice a performance boost from caching frontend assets.
426+
Certain languages and frameworks include more expensive steps that are worth caching. Scala and Elixir are two examples where caching the compilation steps is effective. Rails developers also see a performance boost from caching frontend assets.
344427
345428
Do not cache everything, but _do_ consider caching for costly steps like compilation.
346429

0 commit comments

Comments
 (0)