A dashboard full of cache hits can coexist with a CI pipeline that still feels slow. A hit avoids a task, but developer wait depends on the chain of work that finishes last. Find that chain before optimizing the hit-rate percentage.
Start with the work that blocks completion
A pipeline is a graph: some jobs can run together, while others wait for prerequisites. The critical path is the longest dependent chain through that graph. Queueing, runner startup, and work outside the task graph can add to the wait. The path can change from one run to the next, especially when different tasks miss the cache.
Consider an illustrative run with two independent jobs that start together, no queueing, and a final check that waits for both:
Build: 8 minutes ─┐
Tests: 5 minutes ─┴─ Final check: 2 minutes
Elapsed: 10 minutesIf cached tests take one minute instead of five, the pipeline still takes ten minutes: build remains the longer branch. If build falls to three minutes, the test branch becomes the bottleneck and the pipeline takes seven minutes. These are arithmetic examples, not benchmark results; restore overhead, runner contention, and setup time matter in a real workflow.
A high hit rate can still be valuable when it removes work off the critical path. But counting hits equally hides the difference between a tiny lint task and a long build that gates a release. Nx describes how its task graph and parallel execution fit together: dependencies still constrain what can run at the same time.
Track three different outcomes
- Elapsed time: how long a change waits for the required checks, including queueing and startup.
- Runner usage: the time consumed across jobs, which can fall even when the slowest branch does not.
- Cache usefulness: which expensive tasks were restored, how long restores took, and which misses blocked the critical path.
Parallel jobs make elapsed time and the sum of job durations different quantities. For GitHub-hosted runners, GitHub documents job execution and billable time, including per-job rounding and limitations on where billable minutes are shown. Use your own billing data and runner rates rather than treating a duration reduction as a guaranteed dollar saving.
Run a useful investigation
- Pick a required workflow and comparable changes. Record the commit, runner type, queue and setup time, job dependencies, and total elapsed time.
- Mark the branch that finishes last, then inspect the tasks on it. Separate cache hits, misses, and restore time from work that is not cacheable.
- Compare repeated runs under explicitly recorded cache conditions. A warm local cache and a remote hit are different experiments; do not assume a hit came from the remote service.
- Change one thing at a time. Check both elapsed time and runner usage across several comparable runs, and record variability instead of choosing a single best run.
- Recheck the critical path after each improvement. The next bottleneck may be tests, a deployment gate, setup, or a different task entirely.
Keep correctness first: cache only tasks whose outputs are reproducible from declared inputs. Do not omit an input just to manufacture a hit, and do not cache side-effecting operations such as publishing a release. The Nx inputs and outputs guide covers how to investigate task configuration, while our cache ROI guide separates runner costs from developer wait.
Choose the right lever
Remote caching helps when the same correct task result can be reused across machines. Cachely provides a managed remote cache for supported build tools; the build tool still decides which tasks are cacheable and what belongs in their keys. A miss on changed inputs is normal, not necessarily a defect.
If the critical path is mostly first-time work, setup, queueing, or an inherently sequential check, caching alone will not solve it. Reduce unnecessary work, improve task boundaries, or adjust parallelism where dependencies and resource limits permit. Measure again: the goal is a faster, trustworthy feedback loop, not a prettier cache percentage.