Cutting Bitbucket Pipeline Time with Parallel Steps, Caching, and Branch Workflows
Speed up Bitbucket Pipelines by combining parallel steps, dependency caching, and branch-specific workflows — with a worked YAML example, cost trade-offs, and verification steps.
22 Jul 2025, 14:13 UTC

If your Bitbucket pipeline runs lint, unit tests, and a build one after another, you're paying for wall-clock time you don't need to spend. Independent steps can run at the same time, dependency downloads can be cached between runs, and feature branches don't need the full deploy treatment. This post walks through a single bitbucket-pipelines.yml that combines all three, plus the trade-offs you should know before turning parallelism up.
The problem: a serial pipeline that wastes minutes
A typical first pipeline looks like this: install dependencies, lint, run unit tests, run integration tests, build, deploy. Each step waits for the previous one even when there's no dependency between them. Linting doesn't need test results. Unit tests don't need the integration suite to finish first. The result is a pipeline whose duration is the sum of everything, when it could be closer to the duration of the slowest independent step.
The thesis: most of the win comes from three small YAML changes — a parallel block, a caches declaration, and branch selectors — and each one is verifiable in the pipeline UI.
Parallel steps: run independent work concurrently
Bitbucket Pipelines lets you group steps under a parallel key. Every step in the group starts at the same time, each in its own Docker container. Steps inside a parallel group can't share files directly — if a later step needs build output, pass it through artifacts.
A worked example for a Node.js service. This goes in bitbucket-pipelines.yml at the repository root; no special permissions are needed beyond write access to the repo, and the pipeline runs on Bitbucket's cloud runners by default:
image: node:20\n\ndefinitions:\n caches:\n npm: ~/.npm\n steps:\n - step: &install\n name: Install dependencies\n caches:\n - npm\n script:\n - npm ci\n artifacts:\n - node_modules/**\n\npipelines:\n branches:\n main:\n - step: *install\n - parallel:\n - step:\n name: Lint\n script:\n - npm run lint\n - step:\n name: Unit tests\n script:\n - npm run test:unit\n - step:\n name: Integration tests\n script:\n - npm run test:integration\n - step:\n name: Build and deploy\n script:\n - npm run build\n - echo \"deploy step goes here\"\n feature/*:\n - step: *install\n - parallel:\n - step:\n name: Lint\n script:\n - npm run lint\n - step:\n name: Unit tests\n script:\n - npm run test:unitTwo things to notice. First, the install step produces node_modules as an artifact so the parallel steps don't each reinstall. Second, the feature/* selector gives feature branches a cheaper workflow: lint and unit tests only, no integration suite, no deploy. Branch selectors are how you keep pull-request feedback fast while main gets the full treatment.
Caching: stop downloading the internet every run
The caches section stores a directory between runs. In the example, npm: ~/.npm caches npm's download cache, so npm ci only fetches packages that aren't already stored. Predefined caches exist for common tools (npm, pip, maven, gradle), and you can define custom cache names mapped to any path.
The catch: caches are keyed, and a stale cache can silently give you old dependencies. For npm, the predefined cache is invalidated when package-lock.json changes, which is the behavior you want — but if you define a custom cache, confirm what triggers invalidation. A practical check: change a dependency version, push, and watch the install step's logs. You should see the new package downloaded rather than restored from cache. You can also inspect cache usage in the pipeline UI under the step's \"Caches\" tab, and clear caches manually from Repository settings if you suspect staleness.
Trade-offs and limits worth knowing
Parallel steps are not free. Each step in a parallel group consumes build minutes independently — three 2-minute parallel steps cost roughly the same build minutes as one 6-minute serial step. You save wall-clock time, not money. Parallelism also counts against your plan's concurrent step limit; exceed it and steps queue, which can erase the speedup during busy periods. Check your workspace's build minute usage under Workspace settings before and after the change.
There's also a correctness limit: only parallelize steps that are genuinely independent. If your integration tests mutate a shared database that unit tests also touch, running them concurrently creates flaky failures that are far more expensive than the minutes you saved. Keep shared-state steps serial.
Verify it actually helped
Before changing anything, note the total duration of a typical run in the Pipelines UI. After adding the parallel block and cache, push a trivial commit and compare. You should see the parallel steps' timelines overlapping in the UI, and the install step should be noticeably faster on the second run once the cache is warm. If durations didn't drop, check whether steps are queuing (concurrency limit) or whether the cache is being invalidated on every run (a lock file changing unexpectedly, or a misconfigured custom cache path).
Start with caching — it's the lowest-risk change with an immediate payoff — then parallelize the two or three slowest independent steps, then split branch workflows. Each step is a small, reversible YAML edit, and the pipeline UI gives you a clear before/after picture.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.