Skip to content
Notifications
Clear all

Help: CircleCI cache not restoring properly across branches

3 Posts
3 Users
0 Reactions
1 Views
(@emmab3)
Trusted Member
Joined: 1 week ago
Posts: 38
Topic starter   [#21945]

I'm dealing with a persistent and expensive problem with CircleCI's dependency caching across branches in a mid-sized monorepo (approx 12 services, mix of Node.js and Go). The cache hit rate for `node_modules` and Go module caches is dropping below 20% when workflows are triggered from feature branches, despite using what I believe are the correct restoration keys. This is causing build times to balloon from ~4 minutes on main to over 14 minutes on feature branches, which directly impacts developer velocity and compute costs.

My configuration follows the standard pattern outlined in their docs. Here's a typical job step for Node.js:

```yaml
- restore_cache:
keys:
- v2-node-deps-{{ checksum "package-lock.json" }}
- v2-node-deps-
- run: npm ci
- save_cache:
key: v2-node-deps-{{ checksum "package-lock.json" }}
paths:
- node_modules
```

The theory is that the second, fallback key (`v2-node-deps-`) should restore the most recent cache from *any* branch if an exact checksum match isn't found. My empirical data from the last 30 days shows this is not happening reliably. Instrumentation via the CircleCI API shows cache misses even when the fallback key exists from a successful main branch run hours prior.

I've ruled out the obvious:
* The cache is being saved successfully on `main` (verified via API).
* Branch filtering is not applied.
* Workspace is not relevant hereβ€”this is a standard `restore_cache`/`save_cache` operation.
* The total cache size for the project is under the 500MB limit.

This feels like a platform-level inconsistency. I need a concrete, benchmarked solution. Has anyone performed a systematic analysis of cross-branch cache restoration in CircleCI? Specifically:

1. What is the **actual, observed latency** for a cache written on `main` to become available for restoration on a feature branch? My data suggests it can be >30 minutes, which is untenable.
2. Is there a measurable performance difference between using a single generic fallback key versus a prefix-based approach (e.g., `v2-node-deps-{{ .Branch }}`)?
3. Has anyone successfully implemented a more deterministic pattern, perhaps using the CircleCI API to query and explicitly restore a cache key from `main`?

I will run a controlled benchmark on any proposed patterns and share the build time and cost results here. Vendor documentation is unsatisfyingly vague on the mechanics and SLAs of their global cache distribution layer.


FinOps first, hype last


   
Quote
(@cloud_ops_learner_99)
Reputable Member
Joined: 2 months ago
Posts: 149
 

That fallback key pattern is exactly what I've been using too, and I've seen the same weird misses. 😕 It's supposed to be a "most recent from any branch" but feels more like a "maybe, sometimes" cache.

I had some luck adding the branch name to the fallback? Like `v2-node-deps-{{ .Branch }}-` as a middle-ground key before the totally generic one. It's not perfect, but it helped our hit rate a bit.



   
ReplyQuote
(@alexr)
Estimable Member
Joined: 2 weeks ago
Posts: 93
 

Your configuration is fundamentally correct, but the CircleCI cache restoration logic for generic fallback keys has a nuance the documentation glosses over. The `v2-node-deps-` key will match the *most recently saved cache with a prefix of exactly that string*. However, cache saves from your main branch jobs and feature branch jobs are stored in separate, namespaced object storage paths under the hood. The "most recent" search is scoped to the *current branch's cache namespace*, not globally across all branches.

This is why your instrumentation shows misses even when the data exists - it's in a different logical bucket. The suggestion to add `{{ .Branch }}` to the fallback key is a pragmatic workaround, as it at least ensures you pull a recent cache from the *same* branch lineage. A more aggressive approach is to explicitly save and restore from main's cache key using the CircleCI API in a setup step, though that adds complexity.

For Go modules, this is often worse due to the immutable cache behavior of `go mod download`. A single new dependency can invalidate the entire layer.


Measure twice, cut once.


   
ReplyQuote