How to Speed Up Your Automation Suite Without Creating Flaky Tests
A Playwright test suite can be fast. But as your automation framework grows from 20 tests to 200, 500, or even 1,000 tests, execution time can quickly become a problem. Running every test sequentially means that one test must finish before another can start. With parallel execution, independent tests can run at the same time:
Test 1 ──────────┐Test 2 ──────────┤Test 3 ──────────┤──→ FinishedTest 4 ──────────┤Test 5 ──────────┘
This is where Playwright parallel tests in Java become extremely useful.
However, simply enabling parallel execution is not enough.
If your framework shares the same Page, BrowserContext, or other mutable objects between threads, parallel execution can introduce race conditions, flaky tests, and difficult-to-debug failures.
In this article, we will see how to implement Playwright parallel tests in Java using JUnit 5, how Playwright objects should be managed, and what mistakes you should avoid.
What Are Playwright Parallel Tests in Java?
Playwright parallel testing means executing multiple independent test cases concurrently instead of executing them one after another.
Playwright itself provides browser automation APIs for Java, but when using Java you typically integrate Playwright with a test runner such as JUnit or TestNG.
The official Playwright Java documentation recommends using a test runner such as JUnit and explains that JUnit can be configured to execute tests in parallel.
For example, instead of:
Test A → Test B → Test C → Test D
you can execute:
Thread 1 → Test AThread 2 → Test BThread 3 → Test CThread 4 → Test D
The goal is simple:
Reduce total execution time without sacrificing test reliability.
That second part is important.
Fast but flaky tests are not a successful automation strategy.
Why Should You Run Playwright Tests in Parallel?
Imagine you have 300 UI tests.
If the average execution time of a test is 8 seconds:
300 × 8 seconds = 2400 seconds
That is approximately:
40 minutes
Running suitable tests across multiple threads can significantly reduce the wall-clock execution time.
For CI/CD pipelines, this can make a major difference.
Benefits of parallel execution
- Faster regression testing
- Shorter CI/CD feedback cycles
- Better utilization of CPU resources
- Faster smoke and regression suites
- More efficient browser execution
- Better scalability as the test suite grows
Playwright’s own guidance also emphasizes parallelism and sharding as ways to speed up large test suites.
Playwright Parallel Tests in Java with JUnit 5
JUnit 5 provides built-in support for parallel test execution.
By default, JUnit runs tests sequentially.
To enable parallel execution, we can configure JUnit using the junit-platform.properties file.
Create the following file:
src└── test └── resources └── junit-platform.properties
Add:
junit.jupiter.execution.parallel.enabled=truejunit.jupiter.execution.parallel.mode.default=concurrentjunit.jupiter.execution.parallel.mode.classes.default=concurrent
This enables parallel execution for tests and test classes.
However, there is an important Playwright-specific consideration.
The Most Important Rule: Don’t Share Playwright Objects Between Threads
This is where many parallel Playwright frameworks go wrong.
Consider this architecture:
static Playwright playwright;static Browser browser;static BrowserContext context;static Page page;
It may appear convenient.
But when multiple tests execute simultaneously, multiple threads may attempt to interact with the same Playwright objects.
That can create unpredictable behavior.
The official Playwright Java documentation specifically states that Playwright objects are not safe to use from multiple threads without additional synchronization and recommends creating a Playwright instance per thread and using it exclusively on that thread.
A much better model is:
Thread 1 └── Playwright └── Browser └── BrowserContext └── PageThread 2 └── Playwright └── Browser └── BrowserContext └── PageThread 3 └── Playwright └── Browser └── BrowserContext └── Page
Each thread gets its own Playwright execution context.
Browser vs BrowserContext vs Page
Before designing a parallel framework, it is important to understand these three objects.
Browser
A Browser represents a browser instance such as Chromium, Firefox, or WebKit.
Browser browser;
BrowserContext
A BrowserContext provides an isolated browser session.
BrowserContext context = browser.newContext();
Playwright recommends using a new BrowserContext for each test to keep browser state isolated.
A context can contain its own:
- Cookies
- Local storage
- Session state
- Pages
- Authentication state
Page
A Page represents a browser tab.
Page page = context.newPage();
For parallel execution, you generally want each test to have its own Page and BrowserContext.
Recommended Architecture for Parallel Tests
A good architecture looks like this:
JUnit Test │ ▼Playwright │ ▼Browser │ ▼BrowserContext │ ▼ Page │ ▼ Test
For multiple threads:
Browser
│
┌──────────────┼──────────────┐
│ │ │
Context 1 Context 2 Context 3
│ │ │
Page 1 Page 2 Page 3
│ │ │
Test 1 Test 2 Test 3
This provides isolation while allowing tests to execute concurrently.
Example: Playwright Parallel Tests in Java
Let’s create a simple JUnit 5 example.
Maven Dependencies
A typical Maven project will include Playwright and JUnit 5 dependencies.
<dependencies> <dependency> <groupId>com.microsoft.playwright</groupId> <artifactId>playwright</artifactId> <version>1.61.0</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.13.4</version> <scope>test</scope> </dependency></dependencies>
Keep your dependency versions aligned with the versions supported by your project. The Playwright Java documentation currently shows Playwright
1.61.0in its Maven/Gradle examples.
Creating a Playwright Test Base
One approach is to create a base class responsible for initializing Playwright and the browser.
For example:
import com.microsoft.playwright.*;public class PlaywrightBaseTest { protected Playwright playwright; protected Browser browser; protected BrowserContext context; protected Page page; protected void setup() { playwright = Playwright.create(); browser = playwright.chromium().launch( new BrowserType.LaunchOptions() .setHeadless(true) ); context = browser.newContext(); page = context.newPage(); } protected void tearDown() { if (context != null) { context.close(); } if (browser != null) { browser.close(); } if (playwright != null) { playwright.close(); } }}
The important point is that these objects should belong to the executing test instance/thread rather than being shared as global static objects.
Creating Parallel JUnit Tests
Now let’s create a test class.
import org.junit.jupiter.api.*;@TestInstance(TestInstance.Lifecycle.PER_CLASS)class LoginTests extends PlaywrightBaseTest { @BeforeEach void setUp() { setup(); } @AfterEach void tearDownTest() { tearDown(); } @Test void validLoginTest() { page.navigate("https://example.com"); System.out.println( "Valid Login - " + Thread.currentThread().getName() ); } @Test void invalidLoginTest() { page.navigate("https://example.com"); System.out.println( "Invalid Login - " + Thread.currentThread().getName() ); }}
When parallel execution is enabled, JUnit can execute independent tests concurrently.
Configuring JUnit Parallel Execution
The simplest configuration is:
junit.jupiter.execution.parallel.enabled=truejunit.jupiter.execution.parallel.mode.default=concurrentjunit.jupiter.execution.parallel.mode.classes.default=concurrent
But you should think carefully about what you actually want to parallelize.
You don’t necessarily want every method and every class executing concurrently.
For many UI automation frameworks, a better strategy is to parallelize test classes while keeping methods within a class sequential.
For example:
junit.jupiter.execution.parallel.enabled=truejunit.jupiter.execution.parallel.mode.default=same_threadjunit.jupiter.execution.parallel.mode.classes.default=concurrent
This means:
Class A ├── Test 1 ├── Test 2 └── Test 3Class B ├── Test 4 ├── Test 5 └── Test 6
can execute approximately as:
Thread 1 → Class AThread 2 → Class B
while tests inside each class remain sequential.
This is also the execution model demonstrated in Playwright’s Java JUnit guidance: parallelize classes while keeping test methods within each class on the same thread.
Why Parallelizing Test Classes Can Be Safer
Suppose your framework contains:
LoginTests.javaSearchTests.javaCheckoutTests.javaProfileTests.java
Instead of running:
LoginTests → SearchTests → CheckoutTests → ProfileTests
you could run:
Thread 1 → LoginTestsThread 2 → SearchTestsThread 3 → CheckoutTestsThread 4 → ProfileTests
This approach can make resource ownership easier to reason about.
Each test class can own its Playwright lifecycle.
ThreadLocal: Another Approach
Another common approach in Java automation frameworks is ThreadLocal.
For example:
private static final ThreadLocal<Playwright> playwright = new ThreadLocal<>();private static final ThreadLocal<Browser> browser = new ThreadLocal<>();private static final ThreadLocal<BrowserContext> context = new ThreadLocal<>();private static final ThreadLocal<Page> page = new ThreadLocal<>();
You can initialize objects for the current thread:
playwright.set(Playwright.create());browser.set( playwright.get() .chromium() .launch());context.set( browser.get().newContext());page.set( context.get().newPage());
And retrieve the page:
page.get().navigate("https://example.com");
This approach can work well in larger custom automation frameworks.
However, it also introduces additional lifecycle and cleanup complexity.
You must make sure objects are removed and closed correctly:
page.get().close();context.get().close();browser.get().close();playwright.get().close();page.remove();context.remove();browser.remove();playwright.remove();
If your framework is not large enough to justify this abstraction, simpler per-test or per-class ownership may be preferable.
Parallel Tests and Test Data
Playwright object isolation is only half of the problem.
Your test data must also be isolated.
Imagine two tests execute simultaneously:
Test 1 → Create User → user@test.comTest 2 → Create User → user@test.com
Both tests are using the same email address.
Even if Playwright itself is perfectly isolated, your tests can still fail because the backend sees conflicting data.
This is one of the most common hidden problems with parallel automation.
Bad approach
String email = "testuser@example.com";
for every test.
Better approach
Generate unique test data:
String email = "testuser_" + System.currentTimeMillis() + "@example.com";
Or use a UUID:
String email = "testuser_" + UUID.randomUUID() + "@example.com";
Now each parallel test receives unique data.
Parallel Testing and Database Conflicts
The same principle applies to database testing.
Imagine:
Thread 1 → Update Customer ID 100Thread 2 → Delete Customer ID 100Thread 3 → Validate Customer ID 100
These tests may interfere with one another.
The browser is not the problem.
The test design is.
Before enabling parallel execution, identify shared resources:
- Test accounts
- Database records
- Files
- API data
- Shopping carts
- Orders
- Authentication sessions
- Environment configuration
- External services
If two tests modify the same resource, they may not be good candidates for parallel execution.
Parallel Testing with Page Object Model
Parallel execution does not mean that you need to abandon Page Object Model.
You can continue using a standard POM structure:
src/test/java│├── base│ └── PlaywrightBaseTest.java│├── pages│ ├── LoginPage.java│ ├── HomePage.java│ └── CheckoutPage.java│├── tests│ ├── LoginTests.java│ ├── SearchTests.java│ └── CheckoutTests.java│└── utils └── TestDataUtil.java
The important thing is that Page Objects should not contain shared mutable Playwright state that is used by multiple threads.
For example:
public class LoginPage { private final Page page; public LoginPage(Page page) { this.page = page; } public void login(String username, String password) { page.getByLabel("Username").fill(username); page.getByLabel("Password").fill(password); page.getByRole( AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Login") ).click(); }}
Each test can create its Page Object using its own Page.
LoginPage loginPage = new LoginPage(page);
This keeps the dependency chain clear:
Thread ↓Page ↓Page Object ↓Test
What About BrowserContext?
One of the most useful Playwright features for parallel testing is BrowserContext.
Instead of launching a completely separate browser process for every test, you can create isolated contexts within a browser.
For example:
Browser browser = playwright.chromium().launch();BrowserContext context1 = browser.newContext();BrowserContext context2 = browser.newContext();Page page1 = context1.newPage();Page page2 = context2.newPage();
The contexts are isolated from one another.
This is particularly useful when tests need separate:
- Cookies
- Sessions
- Local storage
- Authentication state
Playwright’s Java documentation recommends a new BrowserContext for each test to provide isolated browser state.
Parallel Tests vs Multiple Browsers
Another interesting question is:
Should parallel tests run on the same browser or different browsers?
For example:
Thread 1 → ChromiumThread 2 → ChromiumThread 3 → FirefoxThread 4 → WebKit
This can be useful for cross-browser testing.
However, remember that each browser introduces additional resource consumption.
A practical CI strategy could be:
Smoke Suite ↓Chromium ↓Parallel executionRegression Suite ↓Chromium + Firefox + WebKit ↓Parallel execution
Playwright supports Chromium, Firefox, and WebKit through its browser automation APIs.
How Many Parallel Threads Should You Use?
More threads do not automatically mean faster execution.
For example:
2 threads → 20 minutes4 threads → 11 minutes8 threads → 7 minutes16 threads → 8 minutes
At some point, additional parallelism can actually make the suite slower because of:
- CPU contention
- Memory consumption
- Browser processes
- Network limitations
- Database limitations
- Environment limitations
Therefore, don’t blindly configure:
100 threads
just because you have 100 tests.
Start with a reasonable number and measure execution time.
Parallel Tests in CI/CD
Parallel execution becomes particularly valuable in CI/CD.
A typical pipeline might look like:
Git Push ↓Build ↓Unit Tests ↓API Tests ↓Playwright UI Tests ↓Parallel Execution ↓Test Report ↓Deployment
Instead of waiting for the complete UI suite to finish sequentially, independent tests can execute concurrently.
This can significantly improve feedback time for developers and QA engineers.
Parallel Testing and CI Resources
There is an important trade-off.
Your local machine may have:
CPU: 8 coresRAM: 16 GB
Your CI runner might have considerably different resources.
Therefore, a configuration that works perfectly locally may overload the CI environment.
A useful strategy is to configure different parallelism levels for different environments.
For example:
Local:4 parallel threadsCI:2 parallel threads
Then increase the CI concurrency only after measuring resource utilization.
Common Mistakes When Running Playwright Tests in Parallel
1. Using Static Page Objects
Avoid:
static Page page;
when multiple threads are using it.
A shared Page can become a synchronization problem.
2. Sharing BrowserContext Between Tests
Avoid unnecessarily sharing:
static BrowserContext context;
between parallel tests.
Each test should ideally have isolated browser state.
3. Reusing the Same Test Data
Avoid:
String username = "testuser";
for every parallel test when the application expects unique users.
Generate unique test data instead.
4. Depending on Test Execution Order
This is a major anti-pattern:
Test 1 → Creates userTest 2 → Logs in as userTest 3 → Deletes user
If these tests are logically dependent, running them in parallel can produce failures.
A better design is:
Test 1 → Creates its own userTest 2 → Creates its own userTest 3 → Creates its own user
Each test becomes independent.
5. Using Shared Files
Suppose every test writes to:
test-output/data.json
Parallel tests can overwrite each other’s data.
Instead, use unique filenames:
test-output/ test-1-data.json test-2-data.json test-3-data.json
6. Ignoring Environment Limits
If your test environment can support only five simultaneous users, configuring 20 parallel tests can cause failures that have nothing to do with Playwright.
Always consider the limits of:
- Application servers
- Databases
- APIs
- CI runners
- Network
- Test environments
How to Debug Parallel Test Failures
Parallel failures can initially look random.
A useful debugging technique is to log the thread name.
System.out.println( "Running test on thread: " + Thread.currentThread().getName());
You can also log:
System.out.println( "Test: " + testName + " | Thread: " + Thread.currentThread().getName());
This helps identify whether a particular thread or resource is causing the problem.
For serious CI failures, combine this with:
- Screenshots
- Video
- Trace files
- Console logs
- Network logs
- Test reports
When Should You NOT Run Tests in Parallel?
Parallel execution is not appropriate for every test.
Avoid parallel execution when tests:
- Depend on each other
- Modify the same records
- Require a single shared account
- Depend on execution order
- Use exclusive external resources
- Are intentionally validating sequential workflows
For example:
Create Order ↓Approve Order ↓Ship Order ↓Cancel Order
This workflow may intentionally require sequential execution.
However, that does not necessarily mean the entire suite should be sequential.
Keep the dependent workflow sequential while running unrelated tests in parallel.
A Practical Parallel Execution Strategy
For a real-world automation framework, I would recommend thinking about parallelism at three levels.
Level 1: Test Independence
First ask:
Can this test run without depending on another test?
If the answer is no, fix the test design before enabling parallel execution.
Level 2: Resource Isolation
Next ask:
Does this test have its own browser context, test data, account, and other required resources?
If not, isolate them.
Level 3: Thread Configuration
Only after the first two are solved should you configure parallel threads.
For example:
Test Suite
│
┌─────────┴─────────┐
│ │
Independent Tests Dependent Tests
│ │
Parallel Sequential
│ │
Thread 1 Flow A
Thread 2
Thread 3
Thread 4
This gives you the performance benefits of parallel execution without forcing every test into parallel mode.
Playwright Parallel Tests in Java: Best Practices
Here are the key practices to follow.
1. Keep tests independent
Every test should be capable of running independently.
2. Avoid shared mutable Playwright objects
Do not casually share Page, BrowserContext, or Playwright objects across threads.
Playwright specifically recommends using a Playwright instance per thread when running JUnit tests in parallel.
3. Use a separate BrowserContext
A new BrowserContext provides isolated browser state.
4. Generate unique test data
Use UUIDs, timestamps, or controlled test-data factories.
5. Avoid test ordering
Tests should not rely on another test completing first.
6. Start with limited concurrency
Measure before increasing the number of threads.
7. Monitor CI resources
CPU and memory constraints can become bottlenecks.
8. Keep reporting thread-safe
Make sure screenshots, logs, and reports are written to unique locations when necessary.
9. Separate sequential and parallel suites
Not every test needs to run in parallel.
10. Design for parallelism from the beginning
Retrofitting parallel execution into a framework full of static objects and shared test data can be much harder than designing for isolation from the start.
Final Thoughts
Playwright parallel tests in Java can dramatically reduce the execution time of a large UI automation suite.
But parallel execution isn’t simply a configuration switch.
The real challenge is test isolation.
You need to think about:
Playwright Objects +Browser Context +Test Data +Database State +Authentication +Files +CI Resources
If these resources are isolated correctly, parallel execution can turn a slow regression suite into a much faster feedback mechanism.
The most important principle to remember is:
Parallel execution exposes weaknesses that sequential execution can hide.
If your framework depends heavily on static objects, shared test data, execution order, or global state, parallel execution will likely expose those problems quickly.
That is not necessarily a bad thing.
It tells you where your automation framework needs better isolation.
And that is ultimately what makes a Playwright framework scalable.
Related Articles
✓ Playwright Cloud Execution BrowserStack & LambdaTest
✓ Playwright Architecture Explained
✓ Playwright Setup with TypeScript
✓ Playwright Locator Strategies
✓ Playwright Auto-Waiting Mechanism
✓ Complete Playwright Tutorial
Discover more from Rotebit
Subscribe to get the latest posts sent to your email.
