WebinarLive on Nov 18: Building a Business Case for PasskeysPasskey Business Case, recording for all registrantsRegister
Back to Overview

Passkeys Performance Testing

Learn to performance-test Passkey (FIDO2/WebAuthn) authentication using k6 and Corbado’s extension. Ensure fast logins and handle traffic spikes seamlessly.

Blog-Post-Author

Stefan

Created: April 29, 2025

Updated: September 23, 2026

Passkeys Performance Testing
Key Facts
  • xk6-passkeys generates WebAuthn registration and login responses with virtual credentials and real cryptographic signatures. No browser or physical authenticator is required for backend load testing.
  • Measure two layers: per-endpoint HTTP latency and the complete simulated ceremony. Neither measures a real user's biometric/PIN interaction or browser experience.
  • Choose your workload: fixed VUs for a simple local test, or a constant arrival rate to target a specified number of ceremonies per second.
  • Use thresholds for CI: checks alone do not fail a k6 run. The examples include success-rate, latency and HTTP-error thresholds, plus a dropped-arrival gate.
  • Test registration and login independently. Local in-memory results cannot establish production capacity or guarantee performance through identity-provider dependencies.

1. Introduction: Why Performance-Test Passkey Authentication?#

Passkeys use a multi-step WebAuthn ceremony: request a challenge, generate a credential or signed assertion on the client, then submit the response for server verification. Load testing should exercise this sequence and the services behind it, including credential storage, gateways and identity-provider integrations.

WhitepaperEnterprise Icon

Enterprise Passkey Whitepaper. Practical guidance, rollout patterns and KPIs for passkey programs.

Get Whitepaper

A virtual authenticator lets a load generator complete these ceremonies without someone touching a fingerprint sensor for every iteration. This makes it useful for measuring backend capacity. It does not test browser UI, device compatibility, passkey synchronization or the time a person takes to approve a request. Use browser-based passkey tests and real devices for those separate questions.

This walkthrough answers three questions:

  • How many registration or login ceremonies can the tested server path sustain?
  • What happens to endpoint and ceremony latency as load increases?
  • Do failures or missed arrivals exceed the service's agreed limits?

Start with a disposable local backend, then adapt the HTTP contract to a staging system. Choose a target using observed peak traffic and agreed headroom, rather than daily average logins. The example rates below are small demonstrations and should not be read as production capacity targets.

For the broader testing strategy, see our passkey testing guide.

2. Tooling Overview: k6 + Passkeys Extension#

2.1 k6 concepts used here#

k6 is a load-testing runner with a JavaScript API.

  • A virtual user (VU) executes a script iteration at a time.
  • An iteration in these examples is one registration or login ceremony.
  • Constant VUs repeatedly run the flow; throughput falls when the flow gets slower.
  • Constant arrival rate schedules new iterations at a configured rate independently of response time, provided enough VUs are available.
  • Thresholds define pass/fail criteria for the test.

A ceremony normally makes two HTTP requests in this demo. Twenty ceremonies per second therefore mean roughly forty HTTP requests per second, excluding login setup and failures that stop before the second request.

2.2 Corbado k6 Passkeys Extension#

The xk6-passkeys repository exposes these JavaScript methods:

  • newCredential(): generates an in-memory ES256 credential with a public/private key pair.
  • newRelyingParty(name, id, origin): creates the relying-party configuration.
  • createAttestationResponse(rp, credential, attestationOptions): creates the registration response as a JSON string.
  • createAssertionResponse(rp, credential, userHandle, assertionOptions): creates the login response as a JSON string.

The options arguments are JSON strings containing a publicKey envelope, as returned by the demo backend. The userHandle is the user's WebAuthn ID, which does not have to be the username. The demo deliberately uses UTF-8 username bytes as that ID.

The extension generates responses in Go; it does not call navigator.credentials.create() or navigator.credentials.get() in a browser. Its authenticator sets user-presence, user-verification and backup flags and uses an Apple iCloud Keychain AAGUID. Those are synthetic test properties and give no evidence of a real Apple device or biometric check. Random keys and challenges also mean the output is not deterministic.

2.3 Build the extension#

Install Go 1.26 or newer, Git and Make. Install Docker to run the containerized demo.

The build pins k6 v1.8.1, xk6 v1.4.13 and virtualwebauthn v1.0.5. The backend uses go-webauthn v0.18.2 and Gin v1.12.0. These are the versions selected for this walkthrough; consult the repository's module files and Makefile when reproducing a run.

git clone https://github.com/corbado/xk6-passkeys cd xk6-passkeys make build ./k6 version

make build creates a local k6 binary with the extension compiled in. Use ./k6 for the following commands.

3. Reference Backend for the Tests#

3.1 What the demo includes#

The Go backend listens on port 8080, stores users and challenges in memory and verifies WebAuthn responses with go-webauthn. It does not issue an application session or access token after login.

This is a disposable test fixture and must never be used as an authentication service. It has no persistent database, user cleanup or cleanup of abandoned challenges. Keep it local, use synthetic identities and restart it between runs. A long registration test continuously grows the user store.

3.2 Endpoint map#

The virtual authenticator runs inside k6, holding the synthetic private keys. The Go server keeps public credentials and pending challenges in memory, without a database. Restarting the server clears its state. Registration creates a credential; subsequent logins reuse it with a fresh challenge.

EndpointPurpose
GET /register/start/:usernameReturn creation options in a publicKey envelope
POST /register/finish/:usernameVerify the credential response and store the credential
GET /login/start/:usernameReturn request options with a fresh challenge
POST /login/finish/:usernameVerify the signed assertion

These are the demo's HTTP routes. There is no universal WebAuthn REST API, so adapting an existing system may require different routes, methods, authentication, cookies, CSRF tokens or payload envelopes.

3.3 Build and start the backend#

docker build -t passkeys-backend examples/backend docker run --rm -p 127.0.0.1:8080:8080 passkeys-backend

Keep that terminal running and execute k6 in another terminal. Without Docker, run go run . from examples/backend.

4. Building the Load-Testing Scenarios#

4.1 Registration#

Each iteration obtains a new challenge, creates a virtual credential, sends its registration response and checks the result. The HTTP flow lives in examples/ceremony.js; the entry point is:

import { register, measure } from "./ceremony.js"; export { options } from "./config.js"; export default function () { measure(register); }

measure() records success and elapsed time even when a request, JSON operation or virtual-authenticator operation throws. The elapsed time includes both HTTP calls and client-side response generation.

4.2 Login#

Before the timed workload, setup() registers a pool of synthetic identities. Each VU then reuses a credential and obtains a fresh challenge for every login:

import exec from "k6/execution"; import { register, login, measure } from "./ceremony.js"; import { userCount } from "./config.js"; export { options } from "./config.js"; export function setup() { return Array.from({ length: userCount }, () => register()); } export default function (users) { const user = users[(exec.vu.idInTest - 1) % users.length]; measure(() => login(user)); }

Credentials are explicitly serialized with JSON.stringify in setup and parsed in the VU, preserving the virtual credential's custom key representation across k6's setup-data boundary. The generated private keys are test material: do not log or commit them.

By default, the pool covers the configured VUs. A smaller USER_COUNT deliberately shares identities, which can produce a different cache and contention pattern. Large pools increase setup time and load-generator memory because k6 copies setup data to VUs. Distributed tests need an appropriate identity partitioning strategy.

4.3 Timing and arrival rate#

The default is two VUs for 30 seconds, with no simulated human delay. Set RATE to use k6's constant-arrival-rate executor:

./k6 run -e RATE=20 -e PRE_ALLOCATED_VUS=10 -e MAX_VUS=40 \ -e DURATION=30s examples/login.js

RATE means ceremonies per second. If the generator lacks free VUs, k6 records dropped_iterations; the requested arrival rate was not fully delivered. Allocate enough VUs and monitor generator CPU/memory before attributing a limit to the backend.

To model a pause between receiving the challenge and sending its response:

./k6 run -e RATE=5 -e PRE_ALLOCATED_VUS=20 -e MAX_VUS=30 \ -e THINK_TIME=2 -e CEREMONY_P95_MS=4000 examples/login.js

THINK_TIME is a fixed delay in seconds, including during setup. It represents a synthetic interaction delay and does not come from any measured biometric interaction. It increases the number of in-flight challenges and the required VUs. Budget approximately arrival rate × ceremony duration in seconds, with headroom. For a larger pool or delay, raise SETUP_TIMEOUT from its two-minute default.

Run both immediate-response and delayed scenarios when relevant. Select the delay from your own journey measurements and adjust the ceremony latency SLO to include it.

5. Running the Tests#

5.1 Local checks#

With the backend running:

./k6 run examples/registration.js ./k6 run examples/login.js

For a short smoke test:

./k6 run -e DURATION=5s examples/registration.js ./k6 run -e DURATION=5s examples/login.js

Use -e VUS=... and -e DURATION=... for these scripts, which configure explicit scenarios. The repository README lists all configuration variables.

A complete run of the arrival-rate login example above, against the Docker backend on a developer laptop (Apple Silicon, k6 v1.8.1), ends with this summary:

█ THRESHOLDS ceremony_duration ✓ 'p(95)<1000' p(95)=14ms ceremony_success ✓ 'rate>0.99' rate=100.00% dropped_iterations ✓ 'count==0' count=0 http_req_failed ✓ 'rate<0.01' rate=0.00% █ TOTAL RESULTS ceremony_duration....: avg=4.84ms min=1ms med=4ms max=27ms p(90)=7.1ms p(95)=14ms ceremony_success.....: 100.00% 600 out of 600 http_req_duration....: avg=1.81ms min=118µs med=1.23ms max=25.44ms p(90)=2.63ms p(95)=3.91ms http_req_failed......: 0.00% 0 out of 1280 http_reqs............: 1280 42.602723/s dropped_iterations...: 0 0/s iterations...........: 600 19.970026/s vus_max..............: 10 passkeys ✓ [ 100% ] 00/10 VUs 30s 20.00 iters/s

Read it as a harness check rather than a benchmark: 600 ceremonies were delivered at the requested 20 per second, none were dropped, and every threshold passed. The 1,280 HTTP requests are 1,200 from the timed login workload plus 80 from registering the 40 setup identities. With the default two constant VUs and no arrival rate, the same in-memory backend completes roughly 3,000 login ceremonies per second on that laptop, which only shows that the generator and extension are not the bottleneck.

5.2 CI thresholds and reports#

The examples define options.thresholds for:

  • ceremony_success: ['rate>0.99']
  • http_req_failed: ['rate<0.01']
  • ceremony_duration: ['p(95)<1000'] by default
  • dropped_iterations: ['count==0'] when arrival-rate mode is enabled

The success check also has a threshold. A failed threshold makes k6 exit with a nonzero status. Checks alone do not fail the run. These are example limits; choose thresholds that match your service and workload.

./k6 run --summary-export=summary.json examples/login.js

The JSON summary records results and does not replace thresholds. Keep the exit status in your CI pipeline. Record the versions, configuration and achieved load alongside it.

5.3 Adapt to staging#

Set BASE_URL, RP_ID and ORIGIN independently. The HTTP service address can differ from the browser origin accepted by the relying party. Changing the URL alone only works if your backend implements the demo contract.

Adapt examples/ceremony.js to your actual API. Preserve the challenge and associated session state from start to finish, and use the original WebAuthn user-handle bytes. For opaque binary user IDs that cannot be represented as UTF-8, adapt the extension boundary; do not substitute the username or pass a base64url string as decoded bytes.

Exercise the complete server path you want to size: application gateways, adapters, identity services and databases can dominate latency even when signature verification is fast. Test a realistic registration/login mix after establishing the separate baselines, and include the actual credential population and storage behavior.

6. Interpreting the Results#

6.1 Endpoint versus ceremony metrics#

MetricInterpretation
http_req_durationIndividual HTTP request time; use endpoint name tags to separate start and finish
ceremony_durationComplete attempted ceremony, including virtual-authenticator work and configured think time
ceremony_successFraction of attempted ceremonies completed successfully, including script errors as failures
http_req_failedHTTP request failure rate
iterationsExecuted script iterations; inspect success separately
dropped_iterationsScheduled arrivals that could not start

Login setup registrations contribute to built-in HTTP metrics, but not to the custom ceremony metrics. Use scenario/name filters in time-series output to isolate the timed login traffic. Stable endpoint names and omission of the URL tag keep randomly generated usernames from creating a metric series per user.

Review p95/p99 and failures together. Fast failures can lower aggregate latency while the service becomes less usable. Check achieved throughput and dropped arrivals before calling a run successful.

6.2 Backend and generator metrics#

Correlate the client results with:

  • CPU, memory, garbage collection and worker/connection pools
  • database query latency, write throughput, locks and cache-hit rates
  • gateway, adapter and downstream identity-service latency and errors
  • challenge-store occupancy, expiry and cleanup
  • load-generator CPU, memory and available VUs

Per-request console logging can distort a local benchmark; the demo disables that access log while keeping panic recovery.

6.3 Limits of local results#

A local in-memory backend avoids production networks, persistent storage and business logic. Its results verify that the harness works; they do not establish production capacity. The sample run in section 5.1 and the thousands of ceremonies per second the demo reaches with constant VUs should therefore not be used as a benchmark or an SLO.

There is no universal 500-1000 ms production expectation. Define separate budgets for the server path and the human-facing journey, then measure against your own topology and traffic. Passing registration load does not guarantee login capacity, or vice versa.

7. How Corbado can help#

The extension covers the mechanics. The harder part is deciding what to test and against which numbers. Most teams we work with have a performance team, an agreed headroom factor and a peak transaction rate for the existing login. What they lack is a mapping from that rate to the new passkey endpoints and everything behind them.

This is where our passkey consulting starts. We go through the complete authentication flow with you and write down which systems a single passkey login touches: gateway, authentication service, identity-provider adapter, credential store and session creation. From the agreed peak rate we derive the expected request volume per system, because one login usually triggers several downstream calls and each system has its own configured limits.

We then adapt the scripts to your implementation. Typical additions are trusted-device checks, device enrollment, MFA and step-up rules, fallback paths and the session that follows a successful login. Every scenario keeps the challenge, tokens and session state together so that the whole path is exercised. Where a dependency has to be simulated, we state which part of the path the result covers and which part it does not.

The load generators run in your own test environment, alongside your existing tools. We help set them up, run baseline and peak scenarios and correlate throughput, latency and failures with your monitoring. You keep a repeatable test setup and a capacity statement for the agreed traffic volume, with the bottlenecks named and the next steps written down. Talk to our team if you want to plan this for your rollout.

8. Conclusion#

Teams that roll out passkeys to a large customer base usually already have a load-testing practice for their login APIs, together with a peak transaction rate and a headroom factor that the business has agreed on. The question is rarely whether to load-test at all. It is how to feed valid WebAuthn traffic into the existing setup when every login needs a fresh challenge and a signed response that only a client can produce.

The extension answers exactly this part. It keeps the challenge and the response together, produces cryptographically valid registrations and logins without a browser, and lets k6 drive them at a chosen arrival rate against the same endpoints, gateways and identity services that production traffic will hit. Registration and login can be sized separately, thresholds turn the run into a pass or fail signal for CI, and the sample run above shows what a healthy result looks like.

In our experience the passkey verification itself is rarely the bottleneck. The surrounding components are: the challenge store, the credential database, the adapters in front of the identity provider and the session logic that follows a successful login. Include them in the tested path, start from a peak rate you have observed and step up from there. Browser and real-device tests remain necessary for the user experience, but they are a separate exercise.

Corbado

About Corbado

Corbado is the Passkey Intelligence Platform for large-scale CIAM teams running consumer authentication. We help you see what IDP logs and generic analytics tools can't: where passkeys, passwords, OTP, social login and fallback journeys succeed, stall or fail, which devices and browsers create friction, and when an OS update silently breaks login. Two products: Corbado Observe layers process mining and observability across authentication journeys. Corbado Connect adds managed passkeys with analytics built in alongside your IDP. VicRoads runs passkeys for 5M+ users with Corbado (more than 80% passkey activation rate). Talk to a Passkey Expert →

Frequently Asked Questions#

How should I structure a k6 load test specifically for the passkey login ceremony?#

Register a pool of synthetic users in setup(), then reuse their serialized credentials in the timed login workload. Each iteration fetches a fresh challenge and submits a signed assertion. The example assigns one identity per VU when the pool covers all VUs.

Is passkey registration always more resource-intensive than login?#

No. Registration writes credentials and may verify attestation, while login verifies assertions and may invoke identity services or application-session logic. Measure both paths independently against realistic traffic and dependencies.

How do I integrate passkey load tests into a CI/CD pipeline using k6?#

Configure options.thresholds to make failed SLOs return a nonzero exit code. The examples gate ceremony success, HTTP failures and ceremony p95, plus dropped iterations in arrival-rate mode. Export a JSON summary for review; checks or a summary file alone do not fail CI.

What response times should I expect from a passkey authentication endpoint under production load?#

There is no universal production latency target. Measure per-endpoint HTTP latency and complete ceremony duration separately against your own SLOs. The virtual authenticator does not measure real biometric interaction, and local in-memory results do not establish production capacity.

See how Corbado fits your passkey rollout and existing authentication stack.

Explore the Console

Share this article


LinkedInTwitterFacebook