ℹ️ Note: Cypress is designed for functional end-to-end testing, not high-volume load testing. However, it is the perfect tool for simulating user behavior to discover and record the exact API sequences your load tests need.

We all know that we should strive for shift-left testing to get fast feedback about application quality and catch bugs early.

However, load testing often hits a wall: What do you do when some APIs involved in a critical user journey aren't ready yet?

Waiting for an environment to be 100% complete before running performance tests delays feedback and pushes critical bottleneck discoveries right up against deadlines.

In this guide, we'll walk through how to shift-left your performance strategy using Cypress to:

  1. Stub missing APIs using cy.intercept so the UI flow isn't blocked.
  2. Observe and capture the sequence of real backend API calls.
  3. Map that blueprint directly into a k6 script to test performance early.

The Scenario: A Blocked User Journey

Let's say we have an application that allows users to manage their books (create, edit, or delete).

To verify the application can handle expected traffic spikes, we want to run a load test simulating a user creating a new book.

[UI: Add Book Page] │ ├──> GET /genres <-- ❌ Unfinished / 500 Error (Blocks UI dropdown) │ └──> POST /api/books <-- ✅ Ready for performance testing! 

The Blocker: Creating a book requires selecting a Genre from a dropdown. But the /genres API isn't finished or deployed yet. Without genre options, the frontend validation blocks the form submission, preventing us from reaching and testing the POST /api/books endpoint.

The Mental Model: Cypress as the Observer

Before diving into the code, keep this model in mind:

  • Cypress is the Observer / Architect: It lives inside the browser, mocks what is broken or missing, navigates through the user actions, and captures the exact network blueprint.
  • k6 is the Builder / Engine: It takes that blueprint and simulates high-concurrency workloads against your backend.

Step 1: Unblock the Flow with cy.intercept

Instead of waiting sprints for the /genres endpoint to be ready, we can stub it directly in Cypress:

// cypress/e2e/load-test-prep.cy.js const mockGenres = [ { id: 1, name: "Classic" }, { id: 2, name: "Science Fiction" }, { id: 3, name: "Fantasy" } ]; it('completes the book creation flow to capture API traffic', () => { // 1. Stub the missing API with pre-defined response data cy.intercept('GET', '**/genres', { statusCode: 200, body: mockGenres, }).as('getGenres'); // 2. Perform user actions in the UI cy.visit('/add-book'); cy.get('#title').type('The Great Gatsby'); cy.get('#author').type('F. Scott Fitzgerald'); cy.get('#genre-select').select('Classic'); cy.get('#submit').click(); // The flow is unblocked and reaches the real backend endpoints! }); 

Step 2: Capture the Network Blueprint

To automatically record every API call triggered during this flow, we can set up a global interceptor that logs requests and outputs an api-sequence.json artifact:

// cypress/e2e/record-apis.cy.ts interface ApiRequestInfo { url: string; method: string; requestHeaders: Record<string, string>; requestBody: any; responseStatusCode: number; } const allApiRequests: ApiRequestInfo[] = []; beforeEach(() => { cy.intercept('**', (req) => { // Only capture target backend service calls if (!req.url.startsWith('http://localhost:3001/api')) { return; } const requestData: ApiRequestInfo = { url: req.url, method: req.method, requestHeaders: req.headers as Record<string, string>, requestBody: req.body, responseStatusCode: 0, }; allApiRequests.push(requestData); req.reply((res) => { const match = allApiRequests.find( (r) => r.url === req.url && r.method === req.method && r.responseStatusCode === 0 ); if (match) { match.responseStatusCode = res.statusCode; } }); }); }); after(() => { cy.writeFile('cypress/fixtures/api-sequence.json', allApiRequests); }); 

The Output Blueprint (api-sequence.json)

Once Cypress completes the run, you get a clean record containing the exact request payloads, headers, and sequence:

[ { "url": "http://localhost:3001/api/books", "method": "POST", "requestHeaders": { "content-type": "application/json" }, "requestBody": { "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "publishedYear": 1925, "genreTypeId": 1, "description": "A classic American novel set in the Jazz Age." }, "responseStatusCode": 201 }, { "url": "http://localhost:3001/api/books", "method": "GET", "requestHeaders": { "accept": "*/*" }, "requestBody": "", "responseStatusCode": 200 } ] 

Step 3: Translate into a k6 Performance Script

With the endpoints, parameters, and flow captured, you can now write a realistic k6 script.

Here is an example running a ramping VUs scenario targeting 10 concurrent users with response time thresholds:

// book-scenario.js import http from 'k6/http'; import { check, sleep } from 'k6'; import { Trend, Rate } from 'k6/metrics'; const getBooksDuration = new Trend('get_books_duration'); const createBookDuration = new Trend('create_book_duration'); const errorRate = new Rate('errors'); const API_BASE_URL = 'http://localhost:3001/api'; const THINK_TIME = 1; export const options = { scenarios: { smoke: { executor: 'per-vu-iterations', vus: 1, iterations: 1, maxDuration: '30s', }, load: { executor: 'ramping-vus', startVUs: 0, stages: [ { duration: '30s', target: 10 }, { duration: '1m', target: 10 }, { duration: '30s', target: 0 }, ], gracefulRampDown: '10s', }, }, thresholds: { http_req_duration: ['p(95)<500'], errors: ['rate<0.1'], }, }; export default function () { // 1. Initial list lookup const getBooksRes = http.get(`${API_BASE_URL}/books`, { tags: { name: 'GetBooks' }, }); getBooksDuration.add(getBooksRes.timings.duration); const getBooksOk = check(getBooksRes, { 'GET /books - status is 200': (r) => r.status === 200, }); errorRate.add(getBooksOk ? 0 : 1); sleep(THINK_TIME); // 2. Create Book (Payload recorded from Cypress blueprint) const payload = JSON.stringify({ title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', publishedYear: 1925, genreTypeId: 1, description: 'A classic American novel set in the Jazz Age.', }); const createBookRes = http.post(`${API_BASE_URL}/books`, payload, { headers: { 'Content-Type': 'application/json' }, tags: { name: 'CreateBook' }, }); createBookDuration.add(createBookRes.timings.duration); const createBookOk = check(createBookRes, { 'POST /books - status is 201': (r) => r.status === 201, 'POST /books - has id': (r) => { try { return JSON.parse(r.body).id !== undefined; } catch { return false; } }, }); errorRate.add(createBookOk ? 0 : 1); // 3. Verify created book appears in list if (createBookOk) { sleep(THINK_TIME); const verifyRes = http.get(`${API_BASE_URL}/books`, { tags: { name: 'VerifyBooks' }, }); const verifyOk = check(verifyRes, { 'GET /books after create - status is 200': (r) => r.status === 200, }); errorRate.add(verifyOk ? 0 : 1); } } 

Step 4: Run and Analyze Results Early

Execute your test using the k6 CLI:

k6 run book-scenario.js 

Early Insights

By testing the ready APIs early, we get instant visibility:

  • If the POST /books duration spikes to 450ms while GET requests stay under 20ms, we immediately know where our database or indexing bottlenecks lie.
  • We identify and fix these latency issues weeks before the /genres service is even finished.

Conclusion

Don't let incomplete environments stall your performance testing.

By using Cypress as an observer, you can stub the missing dependencies, unblock the user journey, record the exact network traffic, and hand off an accurate blueprint to k6.

Shift-left performance testing isn't just about speed—it builds a continuous feedback loop where performance verification becomes a regular part of the development lifecycle rather than a last-minute panic before launch.

Happy testing! 🚀