A 50-scenario UI suite running sequentially takes 10–15 minutes. The same suite on 4 threads finishes in 3–4 minutes. That's the difference between a pipeline that gates every commit and one that developers skip because it's too slow. Parallel execution is not a luxury for large Cucumber suites — it's a requirement.
What Cucumber parallelises
Cucumber 7 with the JUnit Platform engine supports scenario-level parallelism: individual scenarios run concurrently across a thread pool. Each scenario gets its own step definition instances (PicoContainer creates fresh objects per scenario), its own TestContext, and its own browser session. Scenarios are independent by design — parallelism exposes any place where that independence is violated.
Enabling parallel execution
Option 1: junit-platform.properties (the simplest approach)
Create src/test/resources/junit-platform.properties:
cucumber.execution.parallel.enabled=true
cucumber.execution.parallel.config.strategy=fixed
cucumber.execution.parallel.config.fixed.parallelism=4No code changes needed. Run mvn test and 4 scenarios execute simultaneously.
Option 2: Maven Surefire configuration (keeps config in pom.xml)
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<properties>
<configurationParameters>
cucumber.execution.parallel.enabled=true
cucumber.execution.parallel.config.strategy=fixed
cucumber.execution.parallel.config.fixed.parallelism=4
</configurationParameters>
</properties>
</configuration>
</plugin>Option 3: dynamic thread count (scales to available CPUs)
cucumber.execution.parallel.enabled=true
cucumber.execution.parallel.config.strategy=dynamic
cucumber.execution.parallel.config.dynamic.factor=1.0With dynamic strategy and factor 1.0, Cucumber uses one thread per available CPU. Factor 0.5 uses half the CPUs — useful on shared CI agents.
Parallelism strategies
Three execution modes:
# Run scenarios in the same feature file sequentially; features run in parallel
cucumber.execution.parallel.config.strategy=fixed
cucumber.execution.parallel.config.fixed.parallelism=4
# All scenarios run in parallel regardless of feature file
# (default when parallel is enabled — scenarios are the unit)Scenario-level parallelism is more granular — a single large feature file is no longer a bottleneck. Feature-level parallelism is safer for codebases that haven't yet cleaned up shared state.
The non-negotiable: thread-safe step state
Parallel execution breaks every form of shared mutable state. The three patterns to eliminate:
Static WebDriver fields:
// ❌ BROKEN in parallel
public class LoginSteps {
private static WebDriver driver = new ChromeDriver();
}Thread 1 quits the driver. Thread 2 tries to use it. NoSuchSessionException.
Static RestAssured baseURI overrides:
// ❌ BROKEN in parallel — racing writers
RestAssured.baseURI = "https://env1.api.com"; // thread 1
RestAssured.baseURI = "https://env2.api.com"; // thread 2 — overwritesBuild a RequestSpecification per scenario and pass it through TestContext instead of using the global RestAssured statics.
Static counters or accumulators in step definitions:
// ❌ BROKEN in parallel
public class CartSteps {
private static int itemCount = 0;
}With PicoContainer, TestContext and step definition classes are scenario-scoped. If all state is stored in TestContext instance fields (never static), scenarios are isolated and parallelism is safe.
Running specific tag subsets in parallel
Parallel execution composes naturally with tag filtering:
# Run smoke suite on 4 threads, headless
mvn test \
-Dcucumber.filter.tags="@smoke" \
-Dheadless=true \
-Dcucumber.execution.parallel.enabled=true \
-Dcucumber.execution.parallel.config.fixed.parallelism=4Or bake parallel config into dedicated runner classes:
@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "stepdefinitions,hooks")
@ConfigurationParameter(key = FILTER_TAGS_PROPERTY_NAME, value = "@regression and not @wip")
@ConfigurationParameter(key = "cucumber.execution.parallel.enabled", value = "true")
@ConfigurationParameter(key = "cucumber.execution.parallel.config.strategy", value = "fixed")
@ConfigurationParameter(key = "cucumber.execution.parallel.config.fixed.parallelism", value = "4")
public class ParallelRegressionRunner { }Synchronisation for truly shared resources
Most test suites are fully parallel-safe after removing static fields. Occasionally a test needs an exclusive resource — a database sequence, a unique email generator, a rate-limited API key. Use synchronized blocks or AtomicInteger for atomic counters:
public class TestData {
private static final AtomicInteger userCounter = new AtomicInteger(0);
public static String uniqueEmail() {
return "user" + userCounter.incrementAndGet() + "@test.com";
}
}AtomicInteger is thread-safe without synchronized. For complex coordination (waiting until a resource is free), ReentrantLock or Semaphore from java.util.concurrent are appropriate — but these situations are rare if your test data strategy is sound.
Speed impact: parallel vs sequential
UI scenarios don't scale linearly beyond ~4 threads because browser startup and CPU contention become the bottleneck. API scenarios scale much better — 16+ threads is realistic for pure API suites where there's no browser overhead.
⚠️ Common mistakes
- Enabling parallel before removing static state. Run
grep -r "static.*WebDriver\|static.*driver" src/test/before enabling parallelism. Any match is a race condition waiting to happen. - Shared test data in a database. If two parallel scenarios both create a user named "TestUser" and then assert "TestUser exists", one scenario's assertion may see the other's data. Use unique values per scenario: timestamps,
UUID.randomUUID(),AtomicIntegercounters. - Screenshots writing to the same file.
driver.getScreenshotAs(new File("screenshot.png"))in two parallel scenarios overwrites the same file. Attach screenshots to theScenarioobject instead — Cucumber handles unique storage per scenario. @BeforeSuiteand@AfterSuitewith shared state.@Beforeis per-scenario (PicoContainer scope). Suite-level setup (e.g., starting a WireMock server once) goes in a JUnit 5@BeforeAllin the runner class or a separate setup mechanism — not in Cucumber hooks.
🎯 Practice task
Enable and verify parallel execution for your BDD suite. 30–40 minutes.
- Add
junit-platform.propertieswithcucumber.execution.parallel.enabled=trueandparallelism=2. - Search your codebase for
static WebDriverorstatic driver— remove any found. Confirm all driver access goes throughTestContext. - Run
mvn test -Dcucumber.filter.tags="@ui"with 2 threads. Watch 2 Chrome windows open simultaneously in the console output. - Add a log line to
@Beforeand@Afterthat prints the thread name:Thread.currentThread().getName(). Run again — confirm different thread names appear for concurrent scenarios. - Increase to
parallelism=4. Run the full@regressionsuite. Time it with and without parallel and compare. - Stretch: introduce a deliberate race condition by adding a
static int counter = 0;to a step definition class and incrementing it without synchronisation. Run 10 scenarios in parallel. Observe non-deterministic failures or incorrect final counts. Fix withAtomicIntegerand confirm the issue resolves.
Next lesson: reporting — Cucumber HTML, the cucumber-reporting plugin, Allure, and ExtentReports.