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:
currentTokenis 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_failedthreshold will catch it. tokenRefreshesis 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.
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.