Skip to main content

Reuse Your Playwright Tests for Load Testing

Reuse existing Playwright functional tests for load testing instead of building a parallel suite. My own experience, validated by a StarEast 2026 talk.

I'd already reused our existing Playwright functional tests to drive load testing with Artillery.io before I ever heard Andrew Clegg talk about the same idea at StarEast 2026. I hit a wall along the way and landed on an imperfect compromise to get past it. Clegg, a performance engineer at SAS Institute, spent his session "Streamlined Performance Testing with Test Containers" describing an enterprise team that took the same basic idea further, and hit the same kind of wall for the same underlying reasons. Two different tool stacks, two unrelated teams, the same conclusion.

Reduce, Reuse, Recycle: Reusing Functional Tests for Load Testing

Clegg's team learned this lesson the hard way. For about ten years at SAS, performance testing meant LoadRunner: a centralized team of eight engineers, each owning different products, re-recording API workflows before every new build to catch whatever had changed underneath them. It was arduous, API-only, and didn't scale, eight people couldn't keep up with re-recording across all of SAS's heavy-frontend product lines, and some products just didn't get the attention they needed. That re-recording cycle looks to me like the direct cost of drift, though it's not language Clegg used himself: every new build could change which APIs got called, or how, and the old recording quietly went stale the moment it did. Retiring the recording step entirely, in favor of tests QA was already maintaining to support functional testing, removed that tax by design instead of chasing it build after build.

Then SAS reorganized. The centralized team got split apart and embedded into individual product teams. Clegg compares the moment to winning live chickens as a prize on Survivor: technically valuable, useless without the tools to actually process them. Rather than start from scratch and rebuild his own testing infrastructure from zero, he looked at what his new product's QA engineers had already built: hundreds of mature Playwright tests that already knew how to drive the UI. He treated those as something he could buy at the grocery store instead of raising himself.

He frames the whole approach as reduce, reuse, recycle, with recycle deliberately last: rebuilding something to repurpose it is the most expensive option of the three, reuse is what you should reach for first.

My own reasoning got to the same place from a different direction. I wasn't reorganized into a new team; I was tasked with capturing what real users actually experience moving through complicated workflows that span several pages and screens, and I'd already concluded that simulating that with direct API calls was the wrong foundation. Hand-built API load scripts drift silently from what the product actually does over time, as pages get updated and start calling different endpoints or static resources than whatever the script assumed when it was written. Reusing the real functional tests avoids that by construction: if the UI changes what it calls, the test that drives the UI changes with it. Reduce duplicated effort was Clegg's reason to reuse existing tests. Avoiding silent drift was mine. We ended up doing the same thing anyway.

Why Playwright Works for Load Testing

Clegg's case for Playwright as the reuse vehicle: it's open source, its post-failure trace files are unusually rich for diagnosing what actually happened, and its Chrome DevTools Protocol access lets him separate container-level resource usage from the browser's own actual CPU and memory use, rather than inferring browser behavior indirectly from the container's numbers. His concrete example is SAS Studio's flow-builder canvas, a canvas element that's a black box to plain Playwright, no accessible DOM nodes, nothing in a browser snapshot. QA's existing tests already had custom JavaScript hooks built to reach into it. By reusing those tests instead of writing his own, he inherited that hard-won access for free, and just as importantly, inherited QA's ongoing maintenance of it. When the UI changes and something breaks, QA fixes it as part of their own test maintenance instead of the burden falling solely on him.

My own reasoning for choosing Playwright pointed the same direction, though it started from a different problem. We were already transitioning to Playwright for functional testing, and in doing so had solved a set of proprietary automation gotchas specific to our own application, reliably waiting for ExtJS events and working around non-standard UI-blocking patterns that don't behave like a normal DOM. Any other tool would have meant solving those same problems over again from scratch, the same "don't rebuild what's already been fought through" logic Clegg applied to QA's canvas-object hooks. On top of that, Microsoft's continued investment in Playwright's CLI and MCP tooling fit squarely into our own push toward an AI-native SDLC, independent of any performance-testing use case.

Clegg didn't mention this specifically, but this is my own take on why API-only tests are fundamentally the wrong approach if you are trying to collect accurate front-end performance metrics. On a modern web app, a request finishing and a user seeing something useful aren't the same event. A page can download its HTML and initial payload quickly, then have JavaScript execute, discover it needs more data or assets, fetch those too, and only then let the browser finish laying out and painting what the user actually sees, the kind of cascading, browser-side work that a metric like Largest Contentful Paint (when the page's biggest visible element actually renders) exists to measure. Adding up API response times, or even resource-download times, tells you almost nothing about when a real user saw a usable page. That gap barely existed on the simpler, largely static HTML sites performance testing tools were originally built around. This record and playback or API testing mindset seems to be a holdover from simpler times, and the tools and techniques we use need to catch up.

Beyond being more technically accurate, front-end metrics are also simply more legible to non-technical stakeholders than backend ones. "The home page loaded in 4.2 seconds" is easy for anyone to understand. "The inventory endpoint sustained 1,000 requests per minute" doesn't without more context. Reusing UI-driven tests for performance work doesn't just produce more accurate data, it produces self-explanatory data.

That said, there's still a real case for testing API calls directly, to confirm your backend can sustain a given level of traffic at a given response time. Those tests matter for measuring services your front end ultimately depends on. My point is they shouldn't be asked to double as a proxy for front-end performance.

Where Functional and Performance Testing Goals Conflict

Clegg's most actionable slide was the one titled, plainly, "Functional vs Performance tests: Different objectives and expectations." Two problems came up repeatedly:

Hardcoded wait times. A functional test might wait a fixed five or ten seconds for something to finish, harmless under normal conditions. Under load, that same fixed wait can be too short (the action now legitimately takes longer, and the test moves on and fails) or wastefully long (waiting ten seconds for something that finished in a tenth of a second). The deeper problem for performance work specifically: a hardcoded wait sitting inside a measured transaction block corrupts the recorded transaction time itself, you're no longer measuring how long the real operation took, you're measuring your own guess.

Hardcoded resource names. Test files like cars.csv or program_baseline.sas work fine for a single functional run and collide immediately once multiple simulated users hit the same resource name concurrently. Clegg's team's fix, still in progress at the time of the talk, is timestamping or otherwise uniquifying these values wherever they're found.

My team never hit either problem, though not because we anticipated performance testing when we wrote the tests. Our Playwright tests already used data randomization for the test data they created, purely because our test environments have limited ability to tear data down between runs. And we already used Playwright's built in polling explicit waits with large max timeouts instead of fixed sleeps, for ordinary functional-testing reasons (e.g. await page.locator('.success-alert').waitFor({ state: 'visible', timeout: 60000 });). Both decisions turned out to be exactly the guardrails a performance-testing reuse strategy needs. That's worth taking seriously as a general point: writing disciplined functional tests, the kind that avoid brittle fixed waits and brittle fixed test data as a matter of course, is what makes this whole approach viable later, whether or not performance testing is on anyone's mind when those tests get written.

Bugs That UI-Driven Load Testing Catches

Clegg's two bug stories are the strongest evidence for why any of this matters, and both were found specifically because testing drove the real UI rather than a simulated API contract.

The first came out of a two-day endurance test. A single transaction, saving a file, grew steadily from about six seconds to about twenty-four seconds over the course of the run. The cause: the save operation checked for filename collisions by fetching every file in the folder and comparing names one by one on the client side, instead of asking the server a direct "does this name already exist?" question. As the folder accumulated files over two days, that linear scan got proportionally slower. An audience member asked the obvious follow-up: shouldn't API-only testing have caught this too? Clegg's honest answer was that it's not that UI-driven testing finds things API testing structurally can't, it's that the client-side pre-check itself was client logic, invisible to a test that only exercises the underlying endpoints directly.

The second is the sharper one. During a six-hour test window, a burst of failures hit over about twenty minutes. A simulated user scrolled through a paginated file list and never found the file they were looking for. The trace told the story: two "healthy" requests, offset=0 and offset=100, both correctly reported 2,300 total items with a clean HTTP 200. A third request, offset=200, also came back HTTP 200, but reported zero items. The real cause, found by cross-referencing other dashboards, was that the underlying file service had been OOM-killed and was mid-restart. The frontend's request chain hit a connection-refused error against the dead service, and instead of surfacing that failure to the browser, the API silently swallowed it and reported "no files here" with a healthy status code. As Clegg put it: "That's really something we should be passing to the browser so it can then decide what to do. Instead we just tell it, 'Hey, there are no files in that location.' Which to me doesn't seem like a good failure mode."

That's functionally the same danger category as a system reporting false-healthy status because a real failure got silently absorbed somewhere in the stack, discoverable here specifically because the test was exercising what a user actually experiences (scrolling, finding nothing) rather than checking an API contract in isolation. It's the same argument I'd make for why simulating load at the API layer risks missing things: the client-observable behavior and the API's technical response can quietly diverge, and only one of them is what your customer actually lives with.

A third, more incidental finding is worth a mention: because Clegg's team pools test containers from many product teams and runs them together over long windows, they also surfaced a RabbitMQ service leak that only showed up under sustained multi-product load, something no single team's isolated testing would have caught, since some contributing products don't even have a dedicated performance engineer. Pooling containers from many teams buys you high-quality, long-duration integration testing as a side effect of the reuse strategy itself.

Where Playwright Load Testing Stops Scaling

Neither of us pretends this approach scales indefinitely, and the honesty on both sides is worth taking at face value.

Clegg's practical ceiling is around 600 concurrent users, and he's explicit that this is an Azure cost and resource constraint, not a limitation of the technique itself. SAS Studio's real usage pattern is backend-heavy with comparatively few concurrent UI sessions, so it's an appropriate ceiling for his product, not evidence the approach can't go further with more budget.

My own ceiling arrived much sooner. Running the Artillery.io trial from a single workstation, concurrency became constrained by my own laptop, not the server, at somewhere around 10 to 12 concurrent users. Push past that and the test runner's own resource contention started polluting the timing data itself, not just capping how much load I could generate. The actual fix for that ceiling exists: Artillery supports distributed load generation across multiple machines in its licensed tier. Unfortunately we found our environment's network configuration requirements made adopting it problematic compared to our other options. Had we pursued the distributed architecture we probably would have hit a very similar load limit as Clegg.

Different scales, different specific causes, same underlying wall: browser-driven load generation is resource-expensive, and every team running it eventually meets a ceiling that has nothing to do with whether the approach is sound.

Even at 10-12 users, Artillery and Playwright surfaced real hotspots worth investigating.

Two Paths to the Same Hybrid Load Testing Approach

Clegg's explicit recommendation for teams that need higher concurrency than pure UI-driven testing can support is a hybrid: drive bulk load through API tests, and layer a smaller number of these UI-based sessions on top to get real client-side visibility while the system is under that load.

Interestingly, I landed on the same architecture, but not by choosing it. With Artillery's distributed scaling blocked, we moved to K6 instead, which meant giving something up: unlike Artillery, K6 can't run our existing Playwright scripts directly. K6 has its own browser-based test engine with similar-looking syntax, but a fundamentally different approach. Instead of generating load through the UI tests themselves, K6 uses API-driven load to scale up concurrency, then runs browser-based tests to sample the actual front-end experience while the server sits under that API-generated load. It sidesteps the browser-based resource ceiling entirely, at the cost of some realism, the load itself isn't browser-driven, only the sampling is.

Grafana’s own K6 documentation backs this up directly: their recommended hybrid approach describes combining "a small number of virtual users for a browser test with a large number of virtual users for a protocol-level test" as an alternative to full browser-based load testing that’s "much less resource-intensive."

Clegg reached this design as a considered recommendation for other teams. I reached it as a forced compromise after my preferred path got blocked. It's the same architecture either way, arrived at through two unrelated tool stacks by two people who'd never heard of each other. That's a stronger argument for the hybrid model than either of us could have made alone.

Limitations of Reusing Playwright Tests for Load Testing

Clegg's mentioned in closing this isn't free: real effort goes into instrumenting containers and building the supporting infrastructure around them. Quality is mixed once you're borrowing tests from teams outside your own; his immediate team's tests are great, some neighboring teams' are usable, and further out across SAS's org it's "a mixed bag." And he's explicit that this is not true performance tooling: it's a repurposed functional-test tool, and dedicated API-based load tools remain the right choice for genuinely high concurrency, thousands of users, not hundreds.

My own honest limitation is the one I already described: once we moved to K6, we lost the direct reuse benefit that made this appealing in the first place. We're back to maintaining a second set of test logic, just one that shares a language and a syntax style with our functional tests rather than the tests themselves. That said, we're now using AI to streamline the conversion process, which has made maintaining two sets of test logic less painful than it used to be, even though it's still fundamentally two sets of tests.

Takeaway: Reuse Playwright Tests, But Know When to Go Hybrid

Pure API-simulated load is fast to build and easy to scale, but it drifts silently from what your product actually does as the UI changes underneath it, and it can't see client-side logic at all. Pure UI-driven load testing is accurate and reuses work your QA team already did, but it doesn't scale on ordinary hardware, or even on cloud infrastructure, without real cost. The honest answer for most teams sits in between: reuse your functional tests for accuracy where you can, and accept an API-driven hybrid once concurrency demands outgrow what browser-driven load can realistically generate. Two teams that had never heard of each other landed on exactly that answer, from opposite directions, within the same year.