A Karate suite with 200 scenarios that takes 12 minutes to run sequentially can run in 3 minutes with four threads. Parallel execution is built into Karate — no additional library, no complex configuration file. One runner class and one method call. This lesson covers how to enable it, how Karate isolates threads, and how to avoid the pitfalls that break parallelism.
The parallel runner
Sequential execution uses the standard @Karate.Test runner from Lesson 2. Parallel execution uses Runner.path(...).parallel(N) instead:
// ParallelRunner.java
import com.intuit.karate.Results;
import com.intuit.karate.Runner;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ParallelRunner {
@Test
void testParallel() {
Results results = Runner
.path("classpath:users", "classpath:orders", "classpath:products")
.outputCucumberJson(true)
.parallel(4);
assertEquals(0, results.getFailCount(), results.getErrorMessages());
}
}Runner.path(...) takes a list of classpath locations — individual packages, individual feature files, or both. .parallel(4) distributes those feature files across 4 threads. .outputCucumberJson(true) generates the Cucumber-compatible JSON report for third-party dashboard tools.
results.getFailCount() returns the number of failed scenarios across all threads. The assertEquals fails the JUnit test if any Karate scenario failed, which fails the Maven build — exactly what CI needs.
How parallelism is distributed
Karate parallelises at the feature file level. Each feature file runs on one thread from start to finish. Scenarios within a feature file always run sequentially, in order.
This design is intentional. Scenarios within a feature often share state through Background and def — running them in parallel would cause race conditions. Features are designed to be independent of each other, so feature-level parallelism is safe.
With 20 feature files and 4 threads, Karate distributes roughly 5 files per thread. If the files have wildly different scenario counts, the distribution won't be perfectly even — larger files will block their thread longer. Keep feature files balanced: 10–20 scenarios per file is a good target.
Thread isolation
Every thread gets its own karate-config.js execution. Config variables, global headers set by karate.configure(), and callSingle() results are isolated per thread. This means:
- No shared mutable state between threads
callSingle()inkarate-config.jsruns once per thread, not once per suite. For a true once-per-run behaviour in parallel mode, Karate cachescallSingle()calls across threads using a file-based lock — the result is the same, but be aware that the first few threads may all call the login endpoint before the cache is warm.- Environment variables and
karate.envare read per thread, so environment switching works correctly in parallel mode.
Suite-level results
After parallel(4) completes, results contains the aggregate:
System.out.println("Total scenarios: " + results.getScenarioCount());
System.out.println("Passed: " + results.getPassCount());
System.out.println("Failed: " + results.getFailCount());
System.out.println("Elapsed: " + results.getElapsedTime() + " ms");The HTML report at target/karate-reports/karate-summary.html is merged from all threads — it looks identical to a sequential run report, but covers all parallel feature files.
Speed comparison
Estimated runtime — 20 features, ~10 scenarios each, ~3s per scenario
The speedup from 1 to 4 threads is close to linear when feature files are balanced. From 4 to 8, the return diminishes — network latency, API rate limits, and CI runner core count all put a ceiling on how fast parallelism can go. For most projects, 4–6 threads on a standard 2-vCPU GitHub Actions runner is the practical sweet spot.
Running in CI
GitHub Actions example:
- name: Run Karate tests
run: mvn test -Dtest=ParallelRunner
- name: Upload Karate report
if: always()
uses: actions/upload-artifact@v4
with:
name: karate-report
path: target/karate-reports/-Dtest=ParallelRunner tells Maven's Surefire plugin to run the ParallelRunner class specifically — otherwise it may also run the per-package @Karate.Test runners and duplicate the test execution. Using if: always() on the upload step ensures the report is saved even when tests fail.
⚠️ Common mistakes
- Shared mutable external state. If multiple feature files write to the same database record or create a user with the same hardcoded email, concurrent runs will collide. Use the dynamic
uniqueEmailpattern from Chapter 3 —'test-' + java.lang.System.currentTimeMillis() + '@test.com'— to generate collision-free test data. Or use dedicated test data per feature file. - Running both
@Karate.Testrunners andParallelRunnerin the same Maven run. If you haveUsersRunner.java(with@Karate.Test) andParallelRunner.javain the same project,mvn testruns both — each feature file executes twice. Use-Dtest=ParallelRunnerin CI or add<excludes>to the Surefire configuration to prevent duplication. - Setting thread count higher than the API's rate limit allows. 8 threads sending 10 requests per second each is 80 requests per second to the target API. Staging environments, third-party sandboxes, and rate-limited APIs will start returning
429 Too Many Requests, causing flaky failures. Always check the API's rate limit before choosing a thread count.
🎯 Practice task
Run your Karate suite in parallel and measure the difference. 35–45 minutes.
- Create
ParallelRunner.javain your project root package (not insideusers/ororders/). UseRunner.path("classpath:users").outputCucumberJson(true).parallel(2)and theassertEqualscheck. Run withmvn test -Dtest=ParallelRunner. Confirm the test passes and the report appears attarget/karate-reports/. - Add a second package to
Runner.path(...)— even if it's a placeholder feature file with one scenario. Run in parallel and confirm both packages appear in the summary report. - Measure. Add
System.out.println("Elapsed: " + results.getElapsedTime() + "ms")afterparallel(4). Run sequentially first (withparallel(1)), then run withparallel(4). Compare elapsed times. Even with few scenarios, you'll see the overhead structure. - Add
results.getFailCount()andresults.getErrorMessages()to the console output. Force one scenario to fail (wrong assertion). Confirm the parallel runner reports the failure count correctly andassertEqualsfails the JUnit test. - Write a GitHub Actions YAML snippet (
.github/workflows/karate.yml) that runsmvn test -Dtest=ParallelRunnerand uploadstarget/karate-reports/as an artifact withif: always(). You don't need a live repository — writing the YAML correctly is the goal. - Stretch: create
users.featurewith 4 scenarios andposts.featurewith 4 scenarios (using JSONPlaceholder's/postsendpoint). Run withparallel(2). Open the HTML report and confirm scenarios from both feature files appear — evidence they ran simultaneously on different threads.
Next lesson: Karate UI — driving a browser in the same feature file as your API calls.