← All posts
GuideMarch 22, 2026·7 min read·
k6Mockra

Simulating OAuth flows in load tests with stateful mocks

Mock an OAuth token endpoint that issues tokens, returns 401 on expired tokens, and simulates the full refresh flow — at load test scale.

The Problem

You're load testing an API that requires OAuth bearer tokens. Your k6 script does the right thing: it calls your token endpoint first, gets a token, then calls the protected endpoint with that token in the Authorization header.

Two problems emerge immediately.

First: at 1,000 virtual users, your auth server is getting 1,000 simultaneous token requests before a single business API call happens. If your auth server is Auth0, Okta, or an internal service, it will rate-limit you or fall over. You're load testing your auth service, not your application.

Second: token expiry behavior. Real applications handle expired tokens by retrying with a refreshed token. But most mock servers always return 200. They can't simulate a token that works for the first 10 requests, then expires and returns a 401, then gets refreshed. They're stateless.

Mockra's stateful mocks solve both problems.

What Stateful Mocks Are

A stateful mock endpoint cycles through a sequence of responses. You define the steps. Mockra tracks where each endpoint is in the sequence and advances it atomically as requests come in.

Each step has a status_code, a body, optional headers, a repeat count (how many times to serve it before advancing to the next step), and an optional sticky flag (the step repeats forever — useful for terminal states).

A stateful mock endpoint cycles through a sequence of responses. You define the steps. Mockra tracks where each caller is in the sequence. It's like a state machine for your mock — and it's thread-safe across 10,000 concurrent virtual users.

The OAuth Flow We're Simulating

Step Endpoint Response When
1 POST /oauth/token 200 + access_token Token request
2 GET /api/resource 200 + resource data Valid token (first 5 calls)
3 GET /api/resource 401 token_expired Token has expired
4 POST /oauth/token 200 + new access_token Refresh request
5 GET /api/resource 200 + resource data Valid token again

The k6 script handles the 401 response by refreshing the token and retrying — exactly like a real client would.

Step 1: Configure the Token Endpoint Mock

In the Mockra dashboard, navigate to POST /oauth/token and enable stateful responses.

The token endpoint uses a single sticky step — it always returns a valid token. This simulates a functioning auth server handling the load.

Step 1 (sticky — issues tokens forever):

{
  "access_token": "mock_token_valid_abc123",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "read write"
}

Status: 200, Sticky: true

Step 2: Configure the Protected Resource Endpoint

Navigate to GET /api/resource and enable stateful responses. Configure a 3-step sequence:

Step 1 — Valid token, resource returned (repeat 5 times):

{
  "id": "resource_001",
  "name": "Widget",
  "status": "active",
  "created_at": "2026-01-15T09:00:00Z"
}

Status: 200, Repeat: 5

Step 2 — Token expired (repeat 1 time):

{
  "error": "token_expired",
  "error_description": "The access token has expired. Please refresh your token and retry."
}

Status: 401, Repeat: 1

Step 3 — Valid again after refresh (sticky):

{
  "id": "resource_001",
  "name": "Widget",
  "status": "active",
  "created_at": "2026-01-15T09:00:00Z"
}

Status: 200, Sticky: true

The sequence: five successful responses, one 401, then success forever. The last step is sticky by default in Mockra if no repeat is specified.

Step 3: The k6 Script With Token Refresh Logic

// oauth-load-test.js
import http from 'k6/http';
import { check, fail } from 'k6';
import { Counter } from 'k6/metrics';

const MOCK_BASE = 'https://mock.mockra.io/oauth-demo';

export const options = {
  stages: [
    { duration: '1m', target: 100 },
    { duration: '3m', target: 500 },
    { duration: '5m', target: 500 },
    { duration: '1m', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<200'],
    http_req_failed: ['rate<0.01'],
    // Custom counter: token refresh rate should stay low
    'token_refreshes': ['count<1000'],
  },
};

// Counter for token refreshes — shows how often expiry is triggered
const tokenRefreshes = new Counter('token_refreshes');

// Each VU gets its own token (stored in VU-local scope)
let currentToken = null;

function getToken() {
  const res = http.post(
    `${MOCK_BASE}/oauth/token`,
    JSON.stringify({
      grant_type: 'client_credentials',
      client_id: 'mock_client',
      client_secret: 'mock_secret',
    }),
    { headers: { 'Content-Type': 'application/json' } }
  );

  check(res, { 'token issued': (r) => r.status === 200 });
  return res.json('access_token');
}

function callProtectedResource(token) {
  return http.get(`${MOCK_BASE}/api/resource`, {
    headers: { Authorization: `Bearer ${token}` },
  });
}

export default function () {
  // Get token if we don't have one
  if (!currentToken) {
    currentToken = getToken();
  }

  // Call the protected resource
  let res = callProtectedResource(currentToken);

  // Handle token expiry: refresh and retry once
  if (res.status === 401) {
    const errorBody = res.json();
    if (errorBody?.error === 'token_expired') {
      tokenRefreshes.add(1);
      currentToken = getToken();
      res = callProtectedResource(currentToken);
    }
  }

  check(res, {
    'resource returned': (r) => r.status === 200,
    'has resource id': (r) => r.json('id') !== null,
  });
}

Key patterns worth understanding:

  • currentToken is VU-local — each virtual user manages its own token independently. No shared state between VUs.
  • The 401 branch handles expiry — refresh once and retry. If the retry also fails, the http_req_failed threshold will catch it.
  • tokenRefreshes is a custom k6 metric — the threshold ensures expiry isn't triggered so often that it dominates the test results.
  • The state machine in Mockra triggers the 401 after 5 successful requests per endpoint, creating a realistic expiry event distribution across the load test.

Reading the Results

✓ token issued
✓ resource returned
✓ has resource id

http_req_duration......: avg=12ms p(95)=28ms
token_refreshes........: 847

Scenarios: 1 scenario, 500 max VUs, 10m30s max duration
  • P95 of 28ms — response time of the mock server. This is the variable you're eliminating from your real application test.
  • 847 token refreshes — Mockra triggered 401s 847 times across the test, and your client handled every one correctly. Your token refresh logic works under load.
  • 0 failed checks — no unhandled 401s. The retry logic is sound.

Why This Matters

Stateless mock servers always return 200. They can't simulate token expiry. That means you've never actually tested your token refresh logic under load — you've only tested it manually, once, in a browser.

With Mockra stateful mocks, you know that at 500 concurrent users, your refresh logic handles every expiry correctly and doesn't cause a thundering herd of simultaneous refresh requests.

Resetting State Between Runs

The state machine persists between test runs. Before each test run, reset it:

# Reset all endpoint state for your namespace
curl -X POST https://api.mockra.io/workspaces/YOUR_WORKSPACE_ID/reset-state \
  -H "Authorization: Bearer mra_your_api_key"

Or use the "Reset All State" button in the Mockra dashboard on the schema detail page.

Add this to the k6 setup function to reset automatically before each run:

export function setup() {
  // Reset state before test run
  http.post(
    `https://api.mockra.io/workspaces/${__ENV.MOCKRA_WORKSPACE_ID}/reset-state`,
    null,
    { headers: { Authorization: `Bearer ${__ENV.MOCKRA_API_KEY}` } }
  );
}

Pass the env vars at runtime:

k6 run \
  -e MOCKRA_API_KEY=mra_your_key \
  -e MOCKRA_WORKSPACE_ID=your_workspace_id \
  oauth-load-test.js

Wrap-Up

Most mock tools are stateless by design. Mockra's state sequences let you simulate real API behavior across sequential calls — token lifecycle, inventory depletion, payment state — at load test scale.

This works at 500 VUs or 50,000. The state machine is per-endpoint, isolated, and atomic.

Start for free →

Next: Load testing a Stripe integration without hitting Stripe →

Try Mockra free

5,000 requests/month, no credit card required. Upload an OpenAPI spec and get mock endpoints in under 60 seconds.

Start for freeTry error.mockra.io