Skip to main content

Effective Load Testing for SaaS Onboarding

Effective Load Testing for SaaS Onboarding

Load testing a SaaS onboarding flow means testing the chain that runs between “Sign up” and the first useful screen: the signup request, the verification email, the first login, the provisioning of a new workspace and the first-run data. That chain depends on a mail provider, sometimes a payment provider and often a background worker, and each link can fail under a launch-day burst while the signup form itself still looks fine.

This guide covers the load side of that flow. Onboarding design, copy and funnel analytics are a product job and are only touched where they change what you test. For the wider SaaS picture, see how to load test a SaaS platform as it scales and the guide to load testing multi-tenant SaaS applications; this page does not repeat their tenant-mix and isolation material.

Key takeaways

  • Model signups as arrivals per second, not as a fixed number of looping users, so the test keeps arriving when your system slows down.
  • Test the whole chain: signup, verification, first login, workspace provisioning, first-run data. Measure time to “ready”, not only the response time of each call.
  • Make account creation idempotent and prove it by replaying every signup request with the same key.
  • Never send real email or take real payments from a load test. Use a provider sandbox or a stub, and check what rate limits your provider documents.
  • Treat per-tenant provisioning as a queue: its wait time under a burst is usually the first thing to grow.

What is different about load testing onboarding?

Onboarding traffic differs from steady product traffic in three ways. Every request creates state (an account, a workspace, a row per tenant), so a test run leaves data behind and a retry can create it twice. The work is spread across asynchronous steps, so the user-visible wait is the sum of queues, not one response time. And arrivals are bursty: a launch, a newsletter or a Product Hunt day sends many new users within minutes, and none of them has a warm cache.

Step in the flowWhat to measureWhat typically breaks under a burst
Signup requestp95 latency, error rate, duplicate accountsUnique-constraint errors, slow password hashing, retries creating a second account
Verification emailTime from signup to the message arrivingMail provider throttling, a full queue, links that expire before they are sent
Verification click and first loginStatus codes, session creation timeToken lookup on a hot table, session store limits
Workspace provisioningTime from verified to “ready”Background worker backlog, schema or bucket creation per tenant
First-run data seedingTime to the first non-empty screenSample data written one row at a time, template copies, search indexing lag
Third-party calls (billing, CRM, analytics)Latency and error rate of each call from your sideA slow dependency that blocks the signup response

How do you model a signup burst?

Use an open workload model: new users arrive at a set rate whether or not earlier users have finished. A closed model (a fixed pool of virtual users that loop) slows its own arrivals when the system slows down, which hides exactly the failure you want to see. In k6 this is the ramping-arrival-rate executor, which starts iterations on a schedule and adds virtual users as needed (grafana.com/docs/k6, read 2026-10-23).

Build the rate from your own numbers: the signups per hour on your best day so far, multiplied by the growth or campaign you expect, then compressed into the first ten or fifteen minutes. If you have no history, run a ramp until something degrades and report the rate at which it did. The spike testing guide explains the shape, and the scenario design method covers how to pick the mix of journeys.

Real users also pause, abandon the flow and come back later. The guide to simulating realistic user behavior covers think time and drop-off. For onboarding, the useful extra variant is the user who signs up and never verifies: it leaves a pending account and a queued email, and a burst of those loads your database and mail path without ever reaching provisioning.

How do you make account creation idempotent, and test it?

A POST is not idempotent by definition (RFC 9110 lists only the safe methods, PUT and DELETE as idempotent), so a client that times out and retries can create two accounts. The usual fix is an idempotency key: the client sends a unique value in an Idempotency-Key header, and the server returns the saved result for a repeated key instead of running the operation again. Stripe documents the pattern for its API: the first result is stored per key, a retry returns it, reusing a key with different parameters is an error, and v1 keys can be pruned after 24 hours (docs.stripe.com/api/idempotent_requests, read 2026-10-23). The IETF Idempotency-Key header is still an Internet-Draft (draft-ietf-httpapi-idempotency-key-header-08, 2026-10-22), not an RFC. A unique constraint on the email address is the second line of defence, and the glossary entry on idempotency has the short definition.

To test it, have every virtual user send each signup twice with the same key and assert that both responses point at the same account. Also run a test where the second request arrives while the first is still running, which is the case that exposes a race between “check if the email exists” and “insert”.

How do you test email verification without sending real email?

Do not point a load test at a real mail provider. You would send real messages to real inboxes (or to a test domain you do not control), burn your sending reputation, and hit provider limits that are not the limits you are trying to measure. Two options work:

  1. Stub the sender in the test environment. The application writes the message to a store instead of sending it, and a test-only endpoint returns the latest verification token for an address. This tests everything except the provider.
  2. Use the provider’s sandbox or a capture inbox if it has one, and check its documented rate limits and terms before running a burst.

Either way, test the dependency separately for what it adds: a provider call that takes two seconds under load will delay any code path that sends mail inside the request. Look for mail sent synchronously during signup, and for verification links with a short expiry that a backed-up queue can outlive. Measure the time from signup to the message being available, and treat its p95 as a first-class number.

What about payments and other third-party calls?

Onboarding often touches a billing provider (a customer record, a trial subscription), a CRM, an analytics pipeline and sometimes an identity or fraud check. Run these in test or sandbox mode, never against live accounts, and never create real charges from a load test. Two things are worth testing deliberately:

  • A slow dependency. Add latency to the stub for one call and watch whether signup latency follows it. If it does, the call belongs behind a queue.
  • A failing dependency. Return errors from the stub and check that the user still gets an account, the failed call is retried later, and no half-created records are left that block the next attempt.

Provider sandboxes usually enforce their own rate limits, so a high-rate test against one tells you about the sandbox. When that happens, replace the call with a stub that has a realistic delay and keep the real sandbox for a low-rate correctness test.

How do you measure per-tenant provisioning time?

If a signup creates anything per tenant (a database schema, a storage bucket, a default project, API keys), that work usually runs in a background worker. Under a burst, it queues. The number that matters is the time from a verified account to a workspace the user can use, and it is easy to miss because the signup call itself returns early.

Poll a status endpoint (or the read that the first screen uses) until it reports ready, and record the elapsed time as a custom metric. Report its p50 and p95, and watch how both grow as the arrival rate rises. A workspace that is ready in two seconds at one signup per second and in forty seconds at ten per second is a worker-capacity problem you can size for. Multi-tenant concerns such as noisy neighbours are in the multi-tenant guide.

How do you test first-run data seeding?

First-run data (sample projects, templates, demo records, default settings) is often copied at creation time, row by row. It is cheap for one user and expensive for a hundred at once. Check three things: how long the copy takes under a burst, whether the first screen waits for it (an empty dashboard is the failure users notice), and whether search or analytics indexing lags behind the copy. Use the data-driven scripting guide to vary names, locales and plan types, so the test does not hit one cached path.

What does a signup-to-ready load test look like?

The k6 script below runs the whole chain: signup with an idempotency key, a replay of the same request, a token read from a test inbox, verification and a poll until the workspace is ready. It is an illustrative sketch: the URLs, the /test/inbox endpoint and the field names are placeholders for your own staging API. I ran it against a small local mock server (k6 1.7.1) where it passed all three thresholds, which shows the script works, not that your system will.

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend } from 'k6/metrics';
import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js';

const BASE = __ENV.BASE_URL;                 // a staging environment, never production
// The inbox poll answers 404 until the mail lands; do not count that as a failed request.
http.setResponseCallback(http.expectedStatuses(200, 201, 404));

const timeToReady = new Trend('signup_to_workspace_ready', true);

export const options = {
  scenarios: {
    signups: {
      executor: 'ramping-arrival-rate',
      startRate: 1,
      timeUnit: '1s',
      preAllocatedVUs: 50,
      maxVUs: 200,
      stages: [
        { target: 10, duration: '10s' }, // ramp to the launch-day rate
        { target: 10, duration: '20s' }, // hold
      ],
    },
  },
  thresholds: {
    checks: ['rate>0.99'],
    http_req_failed: ['rate<0.01'],
    signup_to_workspace_ready: ['p(95)<5000'],
  },
};

export default function () {
  const email = `loadtest+${uuidv4()}@example.test`;
  const start = Date.now();
  const json = { headers: { 'Content-Type': 'application/json' } };

  const key = uuidv4();                      // one key per signup attempt
  const body = JSON.stringify({ email, password: 'Sup3r-secret-test' });
  const signup = http.post(`${BASE}/api/signup`, body, { ...json, headers: { ...json.headers, 'Idempotency-Key': key } });
  check(signup, { 'signup created': (r) => r.status === 201 });
  const workspaceId = signup.json('workspaceId');

  // Replay the same request: a retry must not create a second account.
  const retry = http.post(`${BASE}/api/signup`, body, { ...json, headers: { ...json.headers, 'Idempotency-Key': key } });
  check(retry, { 'retry returns same workspace': (r) => r.json('workspaceId') === workspaceId });

  // Read the verification token from a test inbox (a stub or sandbox, not a real mail provider).
  let token;
  for (let i = 0; i < 20 && !token; i++) {
    const inbox = http.get(`${BASE}/test/inbox?email=${encodeURIComponent(email)}`);
    if (inbox.status === 200) token = inbox.json('token');
    else sleep(0.25);
  }
  check(token, { 'verification token arrived': (t) => !!t });
  http.post(`${BASE}/api/verify`, JSON.stringify({ token }), json);

  // Wait for per-tenant provisioning and first-run data.
  let ready = false;
  for (let i = 0; i < 20 && !ready; i++) {
    const ws = http.get(`${BASE}/api/workspaces/${workspaceId}`);
    ready = ws.json('status') === 'ready' && ws.json('sampleProjects') > 0;
    if (!ready) sleep(0.25);
  }
  check(ready, { 'workspace ready with sample data': (v) => v === true });
  timeToReady.add(Date.now() - start);
}

The threshold values (rate>0.99, rate<0.01, p(95)<5000) are illustrative; set yours from your own baseline. A failed threshold makes k6 exit non-zero (I measured exit code 99 on a deliberately broken run), so the same script can gate a release. Without the response callback, the 404 polls would count as failed requests and trip http_req_failed on a healthy system, which is the kind of trap that makes a clean run look broken.

Where do test accounts go afterwards?

A signup test creates thousands of accounts. Use a recognisable address pattern on a domain you control or a reserved test domain, tag the accounts, and delete them with a job after the run. Keep tests out of production data and out of your marketing and billing tools: a test account that reaches a CRM or an email list is a real cost. If you must test in production, cap the rate, use a dedicated tenant range and agree it with whoever is on call.

Running it with LoadFocus

LoadFocus runs k6 JavaScript load tests from 25 cloud locations with AI-powered analysis (loadfocus.com/load-testing, read 2026-10-23), so a signup burst can arrive from several regions. Its Create with AI feature can draft a k6 or JMeter script from a plain-English description, which you then review and edit in the built-in editor; review any generated signup script for real side effects before running it. The product pages list the current allowances, and this article prints no prices. Keep the stubbed inbox, sandbox providers and test-account cleanup in your own environment, because those are properties of your system, not of the load tool.

Frequently Asked Questions

What should a SaaS onboarding load test cover?

The whole chain from signup to the first usable screen: the signup request, the verification email, first login, workspace provisioning and first-run data. Measure the time to “ready” as well as the response time of each call, because most of the wait sits in queues rather than in a single request.

How many signups per second should I test?

Start from your own history: the highest signup rate you have seen, multiplied by the growth or campaign you expect, compressed into the first ten to fifteen minutes. With no history, ramp the arrival rate until a threshold fails and report the rate at which it did. Any fixed number from another company is not a target for you.

Should the test send real verification emails?

No. Stub the sender in a test environment or use your provider’s sandbox, and read the token from a test endpoint. A real mail provider adds limits and reputation risk that are not the thing you are measuring, and you would send mail to addresses you do not control.

How do I stop retries from creating duplicate accounts?

Accept an idempotency key on the signup request, store the result per key, and return that result for a repeat, with a unique constraint on the email address as backup. Test it by sending every signup twice with the same key, and once concurrently.

Can I load test with real payment providers?

Use the provider’s test or sandbox mode, never live accounts, and expect the sandbox to have its own rate limits. For a high-rate test, replace the call with a stub that has a realistic delay and keep the real sandbox for a low-rate correctness check.

How is this different from testing a multi-tenant SaaS?

Multi-tenant testing is about tenant mix, noisy neighbours and isolation across existing customers. Onboarding testing is about the creation path for a new tenant: signup, verification, provisioning and seeding. The multi-tenant guide covers the first, this page the second.

Sources

  • k6 documentation, ramping-arrival-rate executor and thresholds (grafana.com/docs/k6, read 2026-10-23).
  • Stripe API reference, idempotent requests (docs.stripe.com/api/idempotent_requests, read 2026-10-23).
  • IETF, The Idempotency-Key HTTP Header Field, Internet-Draft draft-ietf-httpapi-idempotency-key-header-08 (datatracker.ietf.org, read 2026-10-23).
  • RFC 9110, HTTP Semantics, section on idempotent methods (rfc-editor.org).
  • LoadFocus load testing page (loadfocus.com/load-testing, read 2026-10-23).

Chris

Head of Content at LoadFocus

Chris leads content and oversees development across the services this blog covers. The guides and tool comparisons here come out of the same decisions that shape what ships.

Weekly tips right in your inbox.

Don’t miss out on the latest testing tips and tricks, tools and news related to software testing.

Already +20,000 members in the list.

The Load Testing Platform with AI That Explains Your Results

Load test websites and APIs from 25 cloud regions, monitor page speed and uptime, and get AI analysis that explains your results in plain English.

Cloud load testing, website speed and API monitoring in one place.

Hi there! Can I share a cool thing I'm working on with you?

A software testing newsletter that sparks joy.

Our goal is to create helpful content for software developers and testers.

If you're not into it, you can unsubscribe instantly.