Parallel Execution

8 min read

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() in karate-config.js runs once per thread, not once per suite. For a true once-per-run behaviour in parallel mode, Karate caches callSingle() 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.env are 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

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 uniqueEmail pattern 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.Test runners and ParallelRunner in the same Maven run. If you have UsersRunner.java (with @Karate.Test) and ParallelRunner.java in the same project, mvn test runs both — each feature file executes twice. Use -Dtest=ParallelRunner in 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.

  1. Create ParallelRunner.java in your project root package (not inside users/ or orders/). Use Runner.path("classpath:users").outputCucumberJson(true).parallel(2) and the assertEquals check. Run with mvn test -Dtest=ParallelRunner. Confirm the test passes and the report appears at target/karate-reports/.
  2. 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.
  3. Measure. Add System.out.println("Elapsed: " + results.getElapsedTime() + "ms") after parallel(4). Run sequentially first (with parallel(1)), then run with parallel(4). Compare elapsed times. Even with few scenarios, you'll see the overhead structure.
  4. Add results.getFailCount() and results.getErrorMessages() to the console output. Force one scenario to fail (wrong assertion). Confirm the parallel runner reports the failure count correctly and assertEquals fails the JUnit test.
  5. Write a GitHub Actions YAML snippet (.github/workflows/karate.yml) that runs mvn test -Dtest=ParallelRunner and uploads target/karate-reports/ as an artifact with if: always(). You don't need a live repository — writing the YAML correctly is the goal.
  6. Stretch: create users.feature with 4 scenarios and posts.feature with 4 scenarios (using JSONPlaceholder's /posts endpoint). Run with parallel(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.

// tip to track lessons you complete and pick up where you left off across devices.