Test Locks: The Missing Primitive in Parallel E2E Testing
Parallel execution is one of the easiest ways to make an end-to-end test suite faster. Instead of running 100 tests one by one, we can use several workers and execute many tests at the same time.
Playwright is designed for this model. By default, test files can run in parallel using multiple worker processes, and teams can increase parallelism even more with fullyParallel or CI sharding. GitHub
However, there is one important problem:
Not every resource used by an E2E test is isolated.
A browser context can be isolated. A test account can sometimes be created dynamically. But what about a global application setting, a shared database, a legacy external service, or one test environment used by the whole team?
This is where Test Locks become interesting.
Playwright 1.63 introduced native Test Locks, allowing tests to protect shared resources without disabling parallel execution for the whole test suite. GitHub
This article explains why Test Locks matter, how they work, and when they should—and should not—be used.
1. Parallel Tests Are Fast… Until They Share Something
Imagine we have two tests.
The first test changes a global configuration:
test('disable notifications', async ({ page }) => {
await page.goto('/settings');
await page.getByLabel('Notifications').uncheck();
});
The second test expects notifications to be enabled:
test('send notification', async ({ page }) => {
await page.goto('/notifications');
await expect(page.getByText('Notification sent')).toBeVisible();
});
Individually, both tests may pass. But when they run in parallel:

Now the result depends on timing.
Sometimes Test B runs before the setting changes.
Sometimes it runs during the change.
Sometimes it runs after the setting is restored.
Congratulations—we have created a race condition.
This type of failure is especially annoying because rerunning the test may immediately pass.
2. Why Common Solutions Are Not Always Good Enough
There are several ways to avoid this problem.
The simplest solution is to disable parallel execution:
npx playwright test --workers=1
Playwright supports this, but it means the entire test suite becomes sequential.
Another solution is to put tests inside a serial group:
test.describe.configure({ mode: 'serial' });
This can work, but serial mode expresses a different idea: tests execute together in sequence. Playwright also recommends keeping tests independent when possible rather than depending on serial execution.
The real requirement is often more specific:
These tests can run in parallel with everything else, but they must not run at the same time as each other.
That is exactly the problem Test Locks solve.
3. What Is a Test Lock?
A Test Lock is a named shared resource guard.
Consider these tests in different files.
settings.spec.ts
test(
'update user settings',
{ lock: 'user-settings' },
async ({ page }) => {
// Change global user settings
}
);
profile.spec.ts
test(
'rename user',
{ lock: 'user-settings' },
async ({ page }) => {
// Modify the same shared user
}
);
Both tests use: lock: 'user-settings'
Playwright guarantees that they will not execute at the same time, even if they are located in different files, workers, or projects. Other tests without this lock can continue running normally.
Conceptually, the execution looks like this:

The important point is that we are no longer serializing the suite.
We are serializing access to a resource.
That is a much more precise solution.
4. Why Test Locks Are a Useful Primitive
Before native Test Locks, teams often built their own solutions using fixtures, databases, Redis locks, files, environment variables, or custom CI logic.
That adds complexity.
There was also a long-standing Playwright feature request describing exactly this problem: only a small group of tests may require synchronization, while running everything sequentially makes the suite significantly slower.
The Test Lock API makes the intention visible directly in the test:
{ lock: 'payment-gateway' }
This immediately tells another engineer:
– This test uses a shared payment gateway resource that cannot safely handle concurrent test operations.
5. Multiple Locks
A test can require more than one resource:
test(
'reset integration environment',
{
lock: ['database', 'external-api'],
},
async () => {
// ...
}
);
Playwright waits until all required locks are available before starting the test.

6. Locks Can Also Be Applied to Groups
If several related tests need the same lock, repeating the configuration is unnecessary.
A lock can be applied to a test group:
test.describe(
'Admin Settings',
{ lock: 'global-settings' },
() => {
test('change language', async ({ page }) => {
// ...
});
test('change timezone', async ({ page }) => {
// ...
});
}
);
7. An Important Detail: File-Level Execution
There is one behavior that is easy to miss.
n Playwright’s default and serial execution modes, tests inside the same file normally execute together in order. Because of this scheduling model, if one test in the file declares a lock, the lock may be held for the duration of the whole file.
Imagine:
settings.spec.ts
Test A no lock
Test B lock: user-settings
Test C no lock
It is tempting to think only Test B affects the lock.
But depending on the execution mode, the lock can effectively protect the file’s execution unit.
This means file organization still matters.
If one rare test needs a heavily contested resource, moving it into a dedicated file may improve parallel execution.
For example:
tests/
├── profile.spec.ts
├── checkout.spec.ts
├── search.spec.ts
└── global-settings.spec.ts ← locked tests
8. Test Locks Should Not Replace Test Isolation
It is easy to see Test Locks and start adding them everywhere.
That would be a mistake.
The best parallel test is still an independent test.
it may be better to create a unique customer:
test('update customer', async ({ page }, testInfo) => {
const customerName = `customer-${testInfo.testId}`;
// create isolated customer
});
Playwright’s parallelism documentation recommends isolating test data between workers and avoiding shared state whenever possible.
A useful decision process is:

So Test Locks should usually be a last-mile synchronization mechanism, not the default architecture of the test framework.
Conclusion
Parallel execution makes modern E2E suites much faster, but parallelism introduces a difficult problem: shared mutable resources.
Historically, teams often solved this by reducing workers, using serial suites, building custom locking systems, or simply accepting flaky tests.
Playwright 1.63 provides a more focused solution, Test Lock
tests using the same resource can wait for each other while unrelated tests continue running in parallel. Test Locks work across test files, workers, and Playwright projects, and a test can request multiple locks when necessary.
The important lesson is not to lock everything.
A good test architecture should still prefer:

Test Locks do not replace test isolation.
They fill the gap between “everything can run in parallel” and “everything must run sequentially.”
And for large E2E test suites, that small difference can make parallel execution both fast and reliable.