π¨ βTwo Playwright workers bought the last itemβand one failed randomly.β
On a 10-worker grid, isolated browser processes can still race on shared Redis. How do you guarantee state isolation without redesigning InventoryService or SessionService?
π Problem Statement
β’ Worker isolation does not isolate external state.
β Global inventory/session keys create cross-test coupling.
β
One global key set cannot guarantee isolation: namespace or shard.
π‘ Solution & Code Walkthrough
β’ Derive project + workerIndex + testId as a unique tenant.
β’ Send X-Test-Namespace; scope keys and sessions via gateway/service adapter.
β’ Seed before each test; delete only that tenant with afterEach.always.
β’ Use dedicated test Redis. Run stock check β DECR β session SET NX as one Lua transaction; rollback DECR if SET fails.
import { test as base, expect } from '@playwright/test';
export const test = base.extend({
tenant: async ({}, use, ti) => {
const tenant = `${ti.project.name}-${ti.workerIndex}-${ti.testId}`;
await use(tenant);
},
});
test.beforeEach(async ({ request, tenant }) =>
request.put(`/tests/${tenant}`, { stock: 1 }));
test.afterEach.always(async ({ request, tenant }) =>
request.delete(`/tests/${tenant}`));
test('last item reserves once', async ({ request, tenant }) => {
// Service invariant: one Lua transaction; no GET/DECR split.
const reserve = (id: string) => request.post('/checkout/reserve', {
headers: { 'X-Test-Namespace': tenant },
data: { sku: 'SKU-42', orderId: id },
});
expect((await reserve(crypto.randomUUID())).ok()).toBe(true);
expect((await reserve(crypto.randomUUID())).ok()).toBe(false);
});
π Key Takeaways
β
State isolation is a first-class fixture.
β
Cleanup is namespaced and always runs.
β
Atomic writes remove races; retries mask them.
β
No namespace? Shard by SKU or isolate each workerβs cache.
β Quick Summary Q&A
Q: Does worker isolation solve it? A: Noβonly processes are isolated.
Q: Services cannot namespace? A: Shard by SKU or isolate each workerβs cache.
TAGS: playwright, typescript, redis, microservices
ββββββββββββββββββββββββββββββββββββββββ
π± Download the automation guide
App Store & Google Play: ββββββββββββββββββββββββββββββββββββββββ
π² π
πππ ππππππ πππ β πππ+ ππππ π&ππ¬
Practice real-world interview scenarios offline on the free QA Automation & SDET Prep app:
π€ ππ¨π¨π π₯π ππ₯ππ² (ππ§ππ«π¨π’π):
https://play.google.com/store/apps/details?id=com.app.seleniuminterviewquestions&referrer=utm_source%3Ddevto%26utm_medium%3Darticle%26utm_campaign%3Dselenium_20260917
π ππ©π© πππ¨π«π (π’ππ):
https://apps.apple.com/app/id6786760948?pt=128640464&ct=devto_selenium_20260917&mt=8
ββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββ
Top comments (1)
@styrow_dev, treating the namespace as a first-class fixture is the right way to make external-state isolation visible. I wouldnβt make
afterEachcleanup part of correctness, though, because a worker crash or canceled CI job can skip teardown; a run-scoped lease or TTL plus a sweeper gives orphaned namespaces a bounded lifetime. Iβd also issue the two reservation requests concurrently withPromise.all, since sequential calls prove depletion but not contention. How do you make the stock race repeatable enough that the Lua transaction is exercised on every run?