Cutting Bitbucket Pipelines Build Minutes with Dependency Caching and Branch Filters
A practical guide to reducing Bitbucket Cloud build minutes: dependency caching done safely, branch filters, and the trade-off between parallel steps and cost, with a worked bitbucket-pipelines.yml.
05 Apr 2026, 11:08 UTC

If your Bitbucket Cloud bill is creeping up, the biggest lever is usually not a smaller plan — it is making each pipeline run do less work. Two changes in bitbucket-pipelines.yml deliver most of the savings: caching dependency directories so steps stop re-downloading packages, and restricting pipelines to the branches that actually need them. This guide shows a working configuration, explains how the cache behaves, and flags the mistakes that quietly waste minutes or produce stale builds.
How Pipelines caching works
Every step in Bitbucket Pipelines runs in a fresh Docker container built from the image you specify. Nothing persists between steps or runs unless you explicitly cache it. A cache is a named directory that Pipelines compresses, stores after a successful step, and restores into later runs of the same pipeline definition.
Atlassian provides predefined caches for common tools — maven, gradle, node, pip, composer, and dotnetcore — so for a Node project you reference the cache by name rather than declaring a path yourself. For anything else, you define a custom cache under definitions with a name and a directory path.
Two behaviors matter for design:
- A cache is only saved after the first successful step that uses it, and it expires after roughly a week of non-use. Your build must work correctly on a cold cache — caching is a speed optimization, never a correctness requirement.
- Caches have a size limit (commonly documented as 1 GB per cache after compression). Oversized caches may silently fail to save. Cache dependency downloads, not build outputs.
A worked configuration
This example builds a Node project only on main and pull requests, uses the predefined node cache, and runs tests and lint as parallel steps:
image: node:20
pipelines:
branches:
main:
- parallel:
- step:
name: Test
caches:
- node
script:
- npm ci
- npm test
- step:
name: Lint
caches:
- node
script:
- npm ci
- npm run lint
pull-requests:
'**':
- step:
name: PR checks
caches:
- node
script:
- npm ci
- npm test
Three deliberate choices here:
npm ciinstead ofnpm install. It installs strictly frompackage-lock.json, so builds are reproducible even when the cache is cold or stale.- Branch filters (
branches: main:andpull-requests:) mean pushes to feature branches do not consume build minutes at all. On Bitbucket Cloud, build minutes are the billable resource, so skipping runs is worth more than speeding them up. - The two parallel steps shorten wall-clock time, but each consumes its own minutes. If cost matters more than speed, run them sequentially in one step.
Cache node_modules or the package manager cache?
The predefined node cache targets node_modules. That is fast but carries a real risk: if your install step doesn't fully reconcile the cached directory with the lockfile, you can ship stale dependencies. npm ci mitigates this because it deletes node_modules before installing — which, however, throws away much of the caching benefit.
The safer pattern is caching the package manager's download cache and doing a clean install each run:
definitions:
caches:
npm-downloads: ~/.npm
pipelines:
branches:
main:
- step:
name: Build
caches:
- npm-downloads
script:
- npm ci
- npm run build
This avoids network downloads (the slow part) while guaranteeing node_modules is rebuilt from the lockfile every time. The same idea applies elsewhere: cache Maven's ~/.m2/repository, pip's download cache, or Gradle's ~/.gradle/caches rather than compiled output.
Common mistakes and limits
- Caching build artifacts. Output directories bloat the cache past the size limit and can cause the save to fail. Cache inputs (dependencies), not outputs.
- Assuming the cache is always there. First runs, expired caches, and failed saves all produce cold-cache builds. If your step only works with a warm cache, the config is broken.
- Parallel steps as a default. Parallelism reduces elapsed time but multiplies minute consumption by the number of steps. Use it where developer wait time justifies the cost.
- Secrets in the YAML. Never put credentials in
bitbucket-pipelines.yml. Use secured repository or workspace variables, which are masked in logs. - Self-hosted runners differ. Minute accounting and cache persistence work differently on self-hosted runners; verify behavior separately if you use them.
Cache size limits and retention windows are plan- and version-sensitive. Confirm current values in Atlassian's documentation before depending on them.
Verifying the savings
Commit the configuration and run the pipeline twice on the same branch. In the second run's step log, look for cache download lines at the start and an upload message at the end, and compare step durations between runs. Then open the Pipelines usage page in workspace settings to compare minute consumption before and after adding branch filters — that page is the authoritative check on whether the change actually reduced spend.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.