Puppeteer Script Testing: How It Works and What It Measures

A browser window suspended on fine marionette strings with a scripted path traced through it, illustrating automated user-journey testing

· 12 min read

The other nine simulation types in this series answer one question: does your edge hold when hostile traffic arrives. Worth knowing, and not the only question. There is a second one no flood can settle. Does your login still work when a thousand people sign in at once, and does the session store cope.

Puppeteer Script is the entry in Obsidio’s catalogue built for that question. It is not a flood and not measured in requests per second. It takes a script you wrote, runs it against a real browser page on every worker at once, and reports whether each run finished. Synthetic user testing under genuine concurrency, reaching parts of a banking stack raw traffic never gets near.

The short version

  • This one is functional load, not an attack. The platform files it under browser automation. No fixed traffic pattern, because the workload is whatever your script does: a login, a form submission, a full journey.
  • You supply the script. JavaScript runs against a real Chromium page object, one browser per worker, looping until the duration expires. Concurrency comes from the number of workers, nothing else.
  • The metric is iterations, not request rate. Each loop is one attempt with its own timeout. You read per-iteration success and wall-time, and no requests-per-second estimate is shown for this type at all.
  • It reaches the database. This and Browser Flood are the only two of the ten that run your real pages, so they are the only two that exercise the application tier, the authentication path and the session store.
  • Writing the script is not the hard part. Your QA team can already do that. The gap is running it from thousands of real devices in many countries at once.

This is one entry in a series covering each of the ten simulation types Obsidio runs, one post per type. Nine are adversarial. This one is not, which is why it belongs in the set: it covers what the other nine leave unmeasured. For how the categories fit together, start with our guide on how to test your DDoS protection.

The ten simulation types

One post each, grouped the way the platform groups them. The last entry is functional load testing rather than an attack.

Application-layer floods

Slow attacks

Connection exhaustion

Protocol-specific and scripted

  • DNS Floodcoming soon
  • Puppeteer Scriptyou are here

What Puppeteer Script testing actually does

Each worker starts a single Chromium tab and attaches Puppeteer to it. Puppeteer’s documentation describes it plainly: connect() “attaches Puppeteer to an existing browser instance”. The worker gets back a page object, which the docs define as providing “methods to interact with a single tab or extension background page in the browser”. Navigation, clicking, typing, evaluating JavaScript in the page context: all available, because this is a real browser, not a request generator wearing a browser’s user agent.

Your script runs as the body of an async function, once per iteration, in a loop, until the run duration expires. Every iteration is a separate attempt with its own timeout, and an overrunning iteration gets killed and recorded as a failure with the reason attached. A syntax error throws once at startup and fails the whole run.

There is no traffic pattern to describe here, which is what separates this from every other type in the catalogue. A flood has a shape; this has whatever shape your journey has. A payment initiation might be a dozen calls, two of them to systems you do not own.

The plain version: you write a small browser bot that does one job, log in, fill a form, click a button, and the platform clones it onto every worker and runs it on repeat for the duration of the simulation.

What synthetic user testing is good for

Floods answer questions about capacity and filtering. Scripted journeys answer questions about correctness under load, a separate property that fails separately. Grafana’s k6 project, which offers browser-level load testing alongside the protocol-level kind, puts the same point plainly: browser-level testing “provides a way to measure user experience and find issues that are difficult to catch on the protocol level”. Four things are worth testing this way.

Business flow verification under concurrency. The question is not whether the page returned a response but whether the transfer was created and the confirmation showed the right amount. Assertions inside the script record that per iteration, so the result is a pass rate on the flow, not a latency chart.

Authentication and session behaviour. Login is where banks accumulate the most fragile machinery: token issuance, session storage, step-up verification, limits meant to stop credential stuffing. Under a thousand concurrent sign-ins those interact in ways nobody designed, and a protocol-level test replaying a captured token exercises none of it.

The parts of the stack a flood never touches. A slow attack such as Slowloris stops at the web server and never reaches your application. A scripted journey writes to your database, occupies your session store, and calls whatever downstream service the flow depends on.

Your own protective controls, seen from the customer’s side. If bot detection challenges the journey, or your WAF starts blocking a form submission at volume, you have learned how your controls treat genuine automated traffic, which every bank carries in its partner integrations.

Which parts of your stack does this exercise

For the attack simulations, the useful question is which control should have caught it. Here it inverts. Nothing is trying to get past anything, so the question becomes coverage: which layers this test genuinely reaches, and which it leaves alone.

Zone 1

What sits in front, and why it barely matters

  • DDoS protection and scrubbing. Not in play. The traffic volume is ordinary and the test is not trying to look hostile.
  • Firewall and WAF. Sees complete, well-formed requests from your own journey. If it starts blocking form submissions at volume, that is a finding about your customers, not an intruder.
  • Rate limiting. Relevant only if your journey trips a per-account or per-endpoint limit, which some login flows do.
  • Bot detection and challenges. A real browser under automation is the ambiguous case. Only a run against your own configuration shows whether it gets challenged.
  • CDN cache. Absorbs the static parts of the page, quietly reducing how much of the journey reaches your origin.

Zone 2

What the test actually reaches

  • Load balancer or reverse proxy. Carries the traffic without being under pressure.
  • Web server. Serves the pages and their sub-resources at moderate volume.
  • Application tier. The target. Authentication, form handling, business logic, whatever your flow calls downstream.
  • Database and session store. Genuinely exercised, writes included. This and Browser Flood are the only two of the ten that get here, because they run your real pages and journeys.
  • Auto-scaling. Scales on whatever metric you configured. Whether your journey moves one of those metrics is a per-stack answer.

Zone 3

Would you find out what broke?

  • Dashboards. Request rate stays unremarkable throughout. Whether failed logins or slowing transfers show up depends on what you chose to graph.
  • Per-iteration results. What carries this test: iteration success rate and iteration duration, not throughput.
  • Logging and your SIEM. The journey produces ordinary log lines at unusual volume. Check whether anything correlates them into a signal.
  • Infrastructure alerting. Usually silent. A flow returning the wrong answer crosses no CPU or bandwidth threshold.
  • Fraud and security monitoring. Hundreds of simultaneous sign-ins from many countries should register somewhere. If nothing does, that is a finding of its own.

Purple marks what only a run on your own infrastructure can settle. Everything else follows from how the test works.

The parameters, and how this differs from Browser Flood

Four things are configurable, and one does most of the work.

  • The script. The main lever. It receives the page object, a context object carrying the target URL and your parameters, and helpers for delays, logging, named assertions and custom metrics. A slower script means fewer iterations per worker in the same duration.
  • Iteration delay. The pause between iterations, one second by default, adjustable from 100 milliseconds to 10 seconds. It also sets the per-iteration timeout: five times the delay, or 30 seconds, whichever is larger. Raising it slows the loop and gives each iteration more headroom before it is killed.
  • User agent. Pinned for the run if you set it, otherwise whatever the browser runtime ships with.
  • Headers. Extra HTTP headers applied to every request the page makes. Puppeteer documents these as sent “with every request the page initiates”, which is what makes them usable for an auth token or a test-only bypass header.

Two constraints matter. One browser per worker, with no parallelism inside a worker: iterations run back to back, so concurrency is the worker count alone. And it requires the browser runtime on the worker device, with no HTTP fallback. Without the runtime it throws immediately instead of quietly degrading into something less realistic, which is what you want from a test whose whole value is the real browser.

Browser Flood is the sibling that confuses people, since both drive real Chromium. The difference is what they load. Browser Flood loads your page repeatedly and counts every sub-resource the engine fetches, so the load takes the shape of the HTML, CSS, JavaScript, images and background calls your page pulls. Puppeteer Script ignores the sub-resource graph and follows the journey instead: navigate, type, submit, check the result. One produces realistic volume, the other realistic behaviour.

What you can check yourself, and where the gap is

The honest answer here differs from the other nine posts. Your team can script these journeys. If you already do web application load testing with the mainstream tools, or keep end-to-end browser suites in your pipeline, you have the skill and most of the code. The gap sits in two other places.

Concurrency at a realistic scale. Every browser session costs real memory and CPU on the machine driving it. The k6 documentation is direct about the consequence, noting “an additional performance overhead when it comes to spinning up a browser VU” and offering as a less resource-intensive alternative “combining a small number of virtual users for a browser test with a large number of virtual users for a protocol-level test”. That compromise is sensible advice for a CI budget. It also leaves your original question unanswered: what your session store does at a thousand concurrent real journeys.

Geographic and network distribution. A hundred browser sessions from one cloud region share a handful of addresses, one network path and one latency profile. A busy Monday morning looks nothing like that, and that setup cannot tell you how your geo-based rules, bot scoring or fraud monitoring behave when the same flow arrives from thirty countries at once.

So run your own scripted journeys against staging. Just do not read a clean result at low concurrency from one location as evidence the flow holds at scale.

How to run this against production safely

Authorization comes first, and it is not a checkbox. Obsidio verifies domain ownership through a DNS TXT record before any traffic flows, runs ramp up gradually rather than starting at full worker count, and any run can be aborted live. A scripted journey adds one piece of homework: your script creates real records, so decide in advance which account it uses, what it writes and who clears up afterwards. A journey submitting a thousand transfer requests needs the same change-management sign-off as any other production activity.

Where the workers come from is what closes the gap above. Obsidio runs simulations across 100,000+ globally distributed real devices, so a scripted journey arrives from many countries and networks at once rather than from one rented region.

Then watch the right numbers, because two habits carry over badly from flood testing.

  • Ignore request rate. No throughput estimate is shown for this type, deliberately. Read iteration success rate and iteration duration instead. Duration here is the wall-clock time of the whole journey, not the time to first byte of one request, so it compares with what a customer experiences.
  • Assert, do not assume. A page that loads is not a flow that worked. Put named assertions in the script for whatever must be true, the confirmation text, the balance, the redirect target, and read the pass and fail counts. Custom values you report per iteration come back with the results, so extract the reference number proving the transaction happened.

The reporting split is the same as everywhere else. Obsidio measures what is visible from outside: whether each iteration completed, how long it took, which assertions passed, how your edge controls responded. Whether your database connection pool saturated, whether your SIEM correlated anything and whether anyone was paged are answers only your consoles hold. Have someone watching during the window, or the run produces half a finding.

Strength is proven, not promised. A flood asks whether your defences hold. A scripted journey asks whether your bank still works while they do.

Where this fits in a testing programme

A programme built only from attack simulations has a large blind spot. Each of the other nine types tells you something about capacity, filtering or connection handling. None tells you whether the transfer completed. For a regulated institution, resilience means customers could still do what they came for, and verifying that needs a test that does the thing.

The pairing that makes it concrete: run a flood against the front of the stack and a scripted journey alongside it, then see whether the journey still completes while the flood lands. That is as close as a controlled simulation gets to the question a board asks after an incident, and it is the pairing the k6 documentation recommends too, a small browser workload next to a large protocol-level one rather than a choice between them.

For regulated institutions the record matters as much as the result. Obsidio produces cryptographically attested, tamper-evident reports mapped to FINMA, DORA and NIS2, generated inside Trusted Execution Environments, so an authorized functional load run leaves an artifact you can file rather than a screenshot somebody has to vouch for.

To scope an authorized synthetic user test against your own infrastructure, see the Obsidio platform or get in touch. Independent, neutral, verifiable: resilience you can prove.

← Back to Blog