Load & securitybrowse guides

Load testing & security scanning

Put a path under load with k6 and scan it with ZAP: starting a run, reading p95 and error rate, threshold verdicts, findings, and the honest limits.

Your tests answer "does it work". These two answer "does it hold up" and "what is exposed" — and neither is a browser test, which is why they live on their own tab with their own engines.

Load runs go through k6. Security scans go through OWASP ZAP. They sit side by side under Quality → Load & security on a project, and they are deliberately never merged into a single number. A latency percentile and a missing security header are not the same kind of claim, and one score covering both would hide whichever one you needed.

Before the tab appears

Three things have to be true, and they fail in different places:

  1. The surface is switched on. nonFunctional is a platform rollout flag, off by default, flipped in /admin → Runtime. It is flagged because showing this tab shows a customer a button that spends real money against their own infrastructure.
  2. Your plan includes runs. Free and Pro include zero of both. See the table at the bottom.
  3. The project has a base URL. Everything here is measured against it.

And one more that fails later rather than at the door: a worker with k6 and ZAP actually installed has to be running. If no execution queue is up at all, a start is refused immediately with "no non-functional worker". If a worker claims the job but does not have the engine, the run finishes with status error and says so rather than pretending.

Starting a load run

New opens a dialog with two ways to describe what to hit.

Compose

Type target paths, one per line. Every one becomes a GET, between 1 and 100 of them.

FieldRangeDefault
Virtual users1–100010
Ramp-up0–3600 seconds5
Duration1–3600 seconds30

The load profile is a ramp to the requested virtual users, then that many held for the duration.

Derive

Build the scenario from something you already have: a HAR file, or your project's approved contract rules. Contract derivation uses approved rules only — a proposed rule is not the spec yet.

Derive makes you Preview before Start unlocks, and the preview reports two things: the requests it produced, and how many it threw away, split into assets, third-party hosts, other origins, and non-GET requests with no body. A 200-entry recording distilled to four requests looks like a broken importer unless it tells you why it dropped the other 196.

Thresholds

Three, all optional — blank means do not check this, not "check it against zero".

  • p95 in milliseconds
  • Error rate as a percentage
  • Minimum RPS

Set the ones you would actually act on. A threshold you would not treat as a failure is noise in every future result.

What you get back

Start queues the run and returns immediately. If it is refused, the reason renders in the dialog rather than as a disappearing toast: no base URL, no non-functional worker, or the plan limit with a link to plans.

Reading a result

Opening a run polls every four seconds while it is queued or running, and stops as soon as it is terminal.

The metrics are p50 / p95 / p99, RPS, error rate, requests, duration, and the profile that produced them (virtual users, ramp).

A dash is not a zero. Any number k6 did not report renders as rather than being coerced. p99 is usually one of them: k6's summary export carries the median and p95, so a blank p99 means k6 did not produce one, not that nothing was slow.

Each configured threshold gets a row showing its budget beside the measured value, and a verdict:

VerdictMeaning
passMeasured, and inside the budget.
failMeasured, and outside it.
unmeasuredThe number the threshold needed never arrived. Never counted as a pass.

The run's own status follows from those: failed if any threshold failed, error if nothing was measured at all or a threshold went unmeasured, otherwise passed. If you configured no thresholds, the panel says so instead of showing a met state — a run with nothing to check did not check anything.

The download

The download button gives you the scenario you submitted plus the parsed result, as JSON. It is named result, not summary, because it is not k6's summary document: the adapter reads the handful of numbers it needs and deletes its temporary directory, so the raw summary never reaches the row. The panel says so above the button too. If you need k6's own output today, run k6 yourself.

Stopping a load run

Stop is real. The k6 process gets SIGTERM, and SIGKILL five seconds later if it has not exited — it is not a row that flips to "cancelled" while k6 keeps hammering your service for the rest of the half hour.

A cancelled run gets no threshold verdict at all. Not pass, not fail — none. Its metrics are blank, its request count is zero, and it carries a note saying load was applied up to the point it stopped, so nothing in it is a verdict about your service. A late-arriving result from the engine cannot overwrite it either: every terminal write from the worker is conditional on the row still being unfinished.

Security scanning

The security tab runs a ZAP baseline scan: a quick crawl that reports what it can see from outside. It is not a penetration test and it does not exploit anything.

Authorization comes first

You attest, per host, that you are authorized to scan it — a host, a confirmation checkbox, and your name on the record. Attestations are listed, and revocable. Scanning a host with no live attestation is refused.

Scans are capped on purpose: 10 requests per second, 500 URLs, one hour. This is a tool pointed at your own production-shaped infrastructure, and a scanner that can take a service down is a scanner nobody is allowed to run.

A scan can run authenticated by naming an auth role whose session has been captured; the scan replays those cookies. A role with no captured session is refused up front rather than quietly scanning as an anonymous visitor.

Starting a scan is an API call today. The tab attests, lists scans and findings, and cancels — there is no "New scan" button in it yet. Use POST /api/projects/:projectId/security/scans with { "targetUrl": "…" } (and optionally authRoleId) in the meantime.

Findings

Findings are grouped by kind, which is a claim type rather than a severity:

KindWhat it asserts
confirmed-vulnerabilityObserved to be exploitable.
possible-vulnerabilityA scanner thinks so. Nothing was confirmed.
missing-security-headerA header that should be there is not.
functional-authorization-failureA user reached something they should not have.
dependency-cveA dependency with a published advisory.

Everything ZAP reports lands as possible-vulnerability — a baseline scan detects, it does not confirm — and ZAP's own severity string ("High", "Medium") is shown unchanged, attributed to ZAP, rather than being translated into a Vera severity it did not mean. Each finding also carries how it is known: observed, inspected or reported.

One finding is raised per alert type, not per URL, with the occurrence count folded into its title. Twenty-five pages missing the same header is one problem to fix, not twenty-five.

There is no total and no score, deliberately. Counting a missing header and a confirmed vulnerability into one number produces something that goes down when you fix the easy one.

You can also import findings from scanners you already run — Semgrep, Snyk, Trivy, CodeQL — as SARIF, which land as reported.

Stopping a scan

ZAP is a shell script that starts a JVM, so the whole process group is signalled; killing the script alone would leave Java running.

A stopped scan saves no findings at all, including ones it had already collected. A partial report would read as a clean bill of health for every page it never reached, which is a worse outcome than no report.

What your plan includes

PlanLoad runs / monthDAST scans / month
Free00
Pro00
Team42
Enterprise2010

Enterprise is a real ceiling rather than "unlimited". A limit nobody reaches still says the product has one.

A plan with zero disables the New button and explains why; running out of a non-zero monthly allowance is refused when you press Start, inside the dialog.

What these numbers do not prove

Worth being blunt about, because both surfaces are easy to over-read:

  • A load run is HTTP, not browsers. k6 issues requests; it does not execute your JavaScript, render, or hydrate. It measures your backend under concurrency, not what a user's page felt like.
  • It is one origin point. Traffic comes from wherever the worker runs. That is not the geography or the network your users have.
  • A baseline scan is a smoke alarm. Passing it means an automated crawler found nothing obvious in an hour at ten requests a second. It is not a security review, and it says nothing at all about the paths it never reached.

See also