skip to content

What do Cucumber-JVM's parallel execution settings switch on, and what must the glue satisfy first?

level: seniorimportance: should knowfreq 57%

answer

  1. Cucumber's engine has its own switches
  2. Off unless you turn it on
  3. Scenarios, not features, are scheduled
  4. Statics are the first thing to break
  5. Threads inside a JVM, forks multiply JVMs

basics

~20 s

They tell Cucumber's own JUnit Platform engine to run scenarios concurrently inside one JVM, with a strategy that sizes the pool. The unit is the scenario, each Examples row included, so every scenario must own its state.

solid answer

~40 s

`cucumber.execution.parallel.enabled=true` switches concurrency on for Cucumber's engine. It is off by default, and Jupiter's own parallel settings do not control it, because Cucumber is a separate engine with its own keys. `cucumber.execution.parallel.config.strategy` selects `dynamic`, `fixed` or `custom`, sized by `cucumber.execution.parallel.config.dynamic.factor` or `cucumber.execution.parallel.config.fixed.parallelism`. The unit of parallelism is the **scenario**: each `Examples` row of a Scenario Outline is its own scenario and can land on a different thread from its neighbours, so feature files do not run as a block. The glue has to earn that — no mutable static fields, scenario-scoped objects for anything a step writes, `@BeforeAll` and `@AfterAll` understood as once per JVM rather than once per thread, unique names for any file the run writes, and test data created per scenario instead of mutated in place.

code

properties · 3 lines
properties
cucumber.execution.parallel.enabled=true
cucumber.execution.parallel.config.strategy=fixed
cucumber.execution.parallel.config.fixed.parallelism=6

go deeper

for a junior

Know that concurrency is off by default, that a setting turns it on, and that scenarios rather than feature files are what run side by side. Recognising a static field as a hazard is enough here.

for a middle

Explain the strategy options and what sizes the pool, and why Cucumber's keys rather than the surrounding framework's are the ones that matter. Be able to list what shared state breaks first.

for a senior

An interviewer expects a rollout you have actually done: a fixed starting point, repeated runs to expose order coupling, randomised ordering to make it deterministic, and a refusal to tune the thread count to hide a failure.

for a principal

Own the total concurrency budget against the system under test, the split between forks and threads, and the standing rule that a new scenario must create its own data — the convention that keeps the suite parallel-safe as the team grows.

## What the settings actually switch on Cucumber-JVM's engine schedules its own tests, so it carries its own concurrency configuration rather than inheriting the surrounding framework's. | Setting | Effect | |---|---| | `cucumber.execution.parallel.enabled` | master switch; concurrency is off unless this is true | | `cucumber.execution.parallel.config.strategy` | `dynamic`, `fixed` or `custom` | | `cucumber.execution.parallel.config.fixed.parallelism` | the thread count when the strategy is fixed | | `cucumber.execution.parallel.config.dynamic.factor` | multiplier applied to the available processors | | `cucumber.execution.parallel.config.custom.class` | your own strategy implementation | The consequence people trip on: setting Jupiter's parallel properties does nothing here. Jupiter and Cucumber are peer engines on the platform, and a property in one engine's namespace is invisible to the other. A team that "turned parallelism on" and saw no speed-up has usually configured the wrong engine. ## What the unit of parallelism is The scheduled unit is the **scenario**, not the feature file and not the step. A Scenario Outline is expanded before execution, so a 40-row `Examples` table becomes 40 independently scheduled scenarios that may run on 40 different threads. Steps within one scenario always run in order on one thread. That granularity is a gift and a trap. The gift is near-linear speed-up on a wide pack. The trap is that two rows of the same `Examples` table — which the author almost certainly thought of as "the same test with different data" — now execute simultaneously against the same system. ## What has to be true of the glue Concurrency does not make a suite unsafe; it reveals that it always was. The conditions, in the order they bite: 1. **No mutable static state.** A static field holding the current order, session or response is the single most common cause of a suite that passes serially and fails at four threads. State must live in scenario-scoped objects supplied by the dependency-injection container. 2. **No order dependence between scenarios.** One scenario leaving behind data a later scenario relies on is invisible while execution is sequential and lexical, and fatal the moment two scenarios interleave. 3. **`@BeforeAll` and `@AfterAll` are per JVM, not per thread.** Whatever they build is shared by every thread, so it must be thread-safe or must be treated as read-only. 4. **External data must be isolated.** On a vinyl-record marketplace pack, seventeen scenarios that all "add the same first-pressing to the basket" contend for one catalogue row. Each scenario should create the record it needs, keyed by something unique to the run. 5. **No shared file names.** Downloads, screenshots and scratch files written to a fixed path collide silently and produce failures that read as application bugs. 6. **The system under test must take the load.** Six threads against a single test database or a single browser grid slot moves the bottleneck rather than removing it, and the resulting timeouts look exactly like flakiness. ## Two axes people conflate Cucumber's setting multiplies **threads inside one JVM**. The build tool's forking — the surefire fork count, the Gradle test task's maximum parallel forks — multiplies **JVMs**. They compose: three forks at four threads is twelve scenarios in flight, and if you sized each axis separately you have sized neither. Decide the total concurrency you want against the system under test, then pick the split. ## Rolling it out without buying flakiness A pack of 214 scenarios taking 31 minutes is a real target for this, and the safe sequence is boring: - turn it on with a small **fixed** parallelism, so the number is a decision rather than a function of whichever agent picked up the job; - run the pack repeatedly at that setting before raising it, because order coupling shows up as intermittent failure, not as a clean error; - randomise scenario order in a nightly job so hidden dependencies surface deterministically rather than on the day someone adds a scenario; - fix or quarantine the scenarios that fail, and never raise parallelism to make a failure less frequent; - watch a second signal beyond wall clock — a suite that halves its runtime and doubles its retry rate has not got faster. The last point is what separates a senior answer. Parallelism is a change to the correctness assumptions of the suite, not a performance knob, and the interviewer is listening for whether you treat it that way. A browser upgrade landing mid-sprint makes this vivid: the extra concurrency and the new browser build arrive together, and unless you turned parallelism on deliberately and separately, nobody can say which one caused the eleven new intermittent failures.

  • Why choose the fixed strategy over the dynamic one in CI?
    Because a fixed thread count is a decision you can reason about, while a dynamic factor scales with whatever hardware the agent happens to have. A pack tuned on an eight-core agent behaves differently on a sixteen-core one, and the system under test — not the agent — is usually the real constraint. Use dynamic locally, fixed where results must be comparable.
  • A pack passes serially and fails intermittently at four threads. How do you find the cause?
    Assume shared state before suspecting the framework. Search the glue for static mutable fields and for fixed file paths, then check whether any scenario depends on data another one creates. Reproduce with random scenario ordering serially: if it fails there too, the problem is order coupling rather than concurrency, and it is far easier to debug that way.
  • Do build-tool forks and Cucumber's threads solve the same problem?
    No. Forks give each JVM its own statics and its own process isolation, which hides shared-state bugs at the cost of memory and startup time. Threads share one JVM, so they are cheaper and stricter. Most suites end up with a small number of forks and a modest thread count, chosen against the capacity of the system under test rather than the agent.

saying these in an interview costs you the question

  • Expects Jupiter's parallel properties to speed up Cucumber
  • Thinks feature files are the unit of parallelism
  • Keeps shared state in static fields and blames flakiness
  • Treats @BeforeAll as running once per thread
  • Raises the thread count until failures become rare