Parallel Stages and Failure Semantics

Scenario: Independent suites run sequentially for forty minutes while executors sit idle.

Running things "at the same time" only helps if they're truly independent

The obvious appeal of parallel is speed - why run three ten-minute test suites one after another (30 minutes) when you could run them simultaneously (about 10 minutes)? But that speed-up only actually happens if the three suites don't secretly depend on or interfere with each other, and figuring out when that's true is the real skill here.

Analogy: Think of three cooks sharing one kitchen. If each cook has their own stove, own cutting board, and own set of ingredients, working "in parallel" genuinely finishes dinner faster. But if all three cooks need the same single oven, or two of them are both trying to use the same mixing bowl for different recipes, "working in parallel" doesn't actually save time at all - it just creates a lot of waiting and bumping into each other, and possibly someone's recipe gets ruined because another cook grabbed their bowl mid-recipe. Jenkins' parallel branches are the same: they only genuinely help when each branch has its own isolated resources - its own test database, its own port, its own files - and actively hurt when they secretly share something they shouldn't.

Keep this "do the branches truly not depend on each other" question in mind through everything below - it's the deciding factor for whether parallelizing something is a real win or a subtle new source of flaky failures.

Pipeline example

stage('Tests') { failFast true; parallel { stage('Unit') { steps { sh './unit.sh' } }; stage('API') { steps { sh './api.sh' } } } }

Parallelize only genuinely independent work

Declarative parallel executes named sibling stages concurrently, as long as enough eligible executors exist to actually run them at the same time (if only one executor is free, "parallel" branches will just queue behind each other anyway). Every branch needs its own isolated files, network ports, test accounts, and report output paths. Tests that mutate a shared database become order-dependent, intermittently-flaky failures the instant they're parallelized, because now two branches might be reading and writing the same rows at unpredictable times relative to each other. Publish distinct, separately-named results either after all branches complete, or from within each branch's own post block - never write them all to the same shared report path. The overall runtime of a well-parallelized set of branches approaches the longest single branch plus queueing and setup overhead - not zero, and not the naive sum of all branches added together.

[Pipeline] parallel
[Pipeline] { (Branch: Unit)
[Pipeline] { (Branch: API)
[Unit] + ./unit.sh
[API] + ./api.sh
[Unit] 84 passed, 0 failed
[Unit] } // (Unit)
[API] 22 passed, 0 failed
[API] } // (API)
[Pipeline] // parallel

Notice the [Unit] and [API] prefixes interleaving in the log as both branches genuinely run at the same time - and notice the branches don't necessarily finish in the order they started, which is exactly why each branch needs its own isolated output path rather than writing to a shared, unlabeled report file.

failFast trades completeness for speed

failFast true aborts all sibling branches the moment any one of them fails. That's the right call when any single failure already invalidates the whole build regardless of what the other branches would have shown - no point burning ten more minutes of compute to confirm branches that don't matter anymore. But it discards potentially valuable compatibility evidence: if you're testing three runtime versions in parallel and one fails immediately, failFast means you may never find out whether the other two would also have failed for a related reason. Let expensive or informative suites run to completion when seeing the full failure surface (not just the first failure) is more valuable than saving the compute time. Reports need to clearly distinguish a branch that genuinely failed on its own merits from one that was aborted only because a sibling failed - conflating the two makes history misleading.

Capacity limits how much fan-out actually helps

Ten parallel branches competing for only two available executors mostly just creates queueing and repeated per-branch setup overhead - you're not getting ten-way speedup, you're getting two-at-a-time with extra bookkeeping. Third-party APIs, software licenses, shared registries, and even the Jenkins controller's own step-processing capacity can become the actual bottleneck well before raw CPU does. Measure queue time and execution time per branch, then deliberately cap concurrency around whatever resource is genuinely the constraint - guessing at a "safe" parallelism number without measuring tends to either waste capacity or create the exact contention you were trying to avoid.

A pitfall: parallelizing a deployment that isn't idempotent

Do not parallelize a non-idempotent deployment operation against one single target without explicit locks and clearly defined conflict-handling semantics - two branches racing to deploy to the same environment at the same time is a recipe for a corrupted or unpredictable end state, regardless of what each branch's individual log claims succeeded. Retry only the specific branches that failed safely where that's actually possible; restarting the entire fan-out from scratch can re-run - and duplicate the effects of - branches that already completed a real, external, non-reversible action the first time around. Prove genuine isolation between branches with repeated concurrent test runs and deliberately injected failures, not just a single green run that happened not to expose the race condition this time.

Goal: Practice the concrete behavior in jenkins-labeled-parallel-tests. Learn sections have no Jenkins terminal; the Jenkins lab opens the real shared service in a new full-screen tab inside your private folder.