API Performance Testing: Load, Stress and Spike Tests with k6

API Performance Testing: Load, Stress and Spike Tests with k6

An API that answers in 120 ms for one tester can take 9 seconds when 20,000 shoppers hit it at midnight on sale day, or simply fall over. Performance testing finds those limits before your users do. In this lesson you’ll learn the metrics that matter (and why averages lie), the main types of performance tests, how to write a load test in k6, how to add quick latency checks to your Playwright suite, and how to read the results like an engineer.

Quick answerAPI performance testing measures response time, throughput and error rate under different loads. Key types: load (expected peak), stress (beyond the limit), spike (sudden jumps), soak (long duration) and scalability tests. Judge results with percentiles (p95, p99), not averages, against agreed SLAs/SLOs such as “p95 < 800 ms and error rate < 1% at 500 requests/second”. Popular tools: k6, JMeter, Gatling, Locust and Artillery.
Advertisement

The Metrics That Matter

MetricMeaningExample target
Response time (latency)Time from sending the request to receiving the full responsep95 < 800 ms
Percentiles (p50, p90, p95, p99)p95 = 95% of requests were faster than thisp99 < 1.5 s
ThroughputRequests per second (RPS) the system handles500 RPS sustained
Error rate% of failed requests (5xx, timeouts, wrong responses)< 1%
Concurrent users / VUsSimultaneous virtual users2,000
Resource usageCPU, memory, DB connections, queue length on serversCPU < 70% at peak
Time to first byte (TTFB)Server think time before the response startsDiagnoses slow backends
Why averages lie: the bank queue Nine customers are served in 1 minute each; one waits 51 minutes because the system hung. The average is 6 minutes, which sounds fine. The p95/p99 is 51 minutes, which is what the angry customer posts on social media. A real API with an average of 200 ms can still have 1 in 100 users waiting 8 seconds. Always report percentiles.

Types of Performance Tests

TypeLoad patternQuestion it answersReal example
Smoke (performance)1 to 5 users, shortDoes the script work, are basics fast?Run on every build
LoadRamp to expected peak, holdDo we meet SLAs at normal peak?Evening dinner rush for a food app
StressKeep increasing past peakWhere’s the breaking point, how does it fail?Find max orders/second
SpikeSudden jump and dropDo we survive and recover?Flash sale opens at 12:00, ticket booking opens
Soak / enduranceModerate load for hoursMemory leaks, connection exhaustion24-hour run before a festive season
ScalabilityLoad while adding serversDoes adding capacity actually help?Auto-scaling validation
Advertisement

Before You Load Test: Ground Rules

Never load test what you don’t own Load testing a system without permission can look like a denial-of-service attack. Use a dedicated performance environment, agree the time window with the team, and tell the operations team. The SQA Tools practice API allows 600 requests per minute per IP and is shared by many learners, so only run the tiny examples in this lesson against it (a few virtual users with pauses), never real load.
  1. Define goals: “p95 < 800 ms, errors < 1% at 300 RPS for 30 minutes”.
  2. Model real traffic: which endpoints, in what mix (e.g. 70% browse, 20% search, 10% checkout), think time between calls.
  3. Production-like environment: similar data volumes, configuration and infrastructure.
  4. Test data: enough unique users, products and tokens so caches don’t make results unrealistically good.
  5. Monitoring: watch server metrics (APM, dashboards) during the test, not just client-side numbers.
Advertisement

Your First k6 Load Test

k6 is a free, open-source load testing tool where scripts are written in JavaScript. Install it from k6.io (Windows, macOS, Linux or Docker), then save this as users-load.js:

import http from 'k6/http';
import { check, sleep } from 'k6';

const BASE = 'https://sqatools.in/wp-json/sqa-api/v1';

export const options = {
  vus: 3,                 // 3 virtual users - deliberately tiny for the shared practice API
  duration: '30s',
  thresholds: {
    http_req_duration: ['p(95)<1500'],   // 95% of requests faster than 1.5 s
    http_req_failed: ['rate<0.01'],      // less than 1% errors
    checks: ['rate>0.99'],
  },
};

export default function () {
  const res = http.get(`${BASE}/users?per_page=5`);
  check(res, {
    'status is 200': (r) => r.status === 200,
    'has 5 users': (r) => r.json('data').length === 5,
  });
  sleep(1);               // think time: a real user pauses between actions
}
k6 run users-load.js

k6 prints a summary including http_req_duration (avg, min, med, max, p90, p95), http_reqs (throughput), http_req_failed and whether each threshold passed. A failed threshold gives a non-zero exit code, so k6 can gate a CI pipeline.

Ramping load with stages (load, stress, spike)

// For YOUR performance environment, not the practice API
export const options = {
  stages: [
    { duration: '2m', target: 100 },    // ramp up to 100 users
    { duration: '10m', target: 100 },   // hold: load test
    { duration: '2m', target: 400 },    // push beyond: stress
    { duration: '30s', target: 1500 },  // sudden spike
    { duration: '2m', target: 0 },      // ramp down: does it recover?
  ],
  thresholds: { http_req_duration: ['p(95)<800'], http_req_failed: ['rate<0.01'] },
};

A realistic user journey

import http from 'k6/http';
import { check, group, sleep } from 'k6';

export default function () {
  let token;
  group('login', () => {
    const res = http.post(`${__ENV.BASE}/auth/login`,
      JSON.stringify({ username: __ENV.USER, password: __ENV.PASS }),
      { headers: { 'Content-Type': 'application/json' } });
    check(res, { 'login 200': (r) => r.status === 200 });
    token = res.json('access_token');
  });
  sleep(2);
  group('browse and order', () => {
    const auth = { headers: { Authorization: `Bearer ${token}` } };
    check(http.get(`${__ENV.BASE}/products?category=books`, auth), { 'browse 200': (r) => r.status === 200 });
    sleep(3);
    // ... add to cart, checkout
  });
}

Run with k6 run -e BASE=https://perf.example.com/api -e USER=perf_user_1 -e PASS=... journey.js; secrets come from the environment, not the script.

Advertisement

Reading Results: A Worked Example

StageUsersRPSp95ErrorsInterpretation
Load100310420 ms0.1%Meets SLA comfortably
Stress4005202.8 s0.4%Throughput flattens while latency climbs: saturation point around 500 RPS
Spike1,5005409.5 s18% (503/504)Overload; check auto-scaling and queueing
Recovery0–––Latency back to normal within 1 minute = good

The pattern to recognise: when throughput stops growing but response time keeps rising, you’ve found a bottleneck (CPU, DB connection pool, a slow query, a lock, a downstream API). Pair the timing with server metrics to locate it.

Rate Limits, Timeouts and Retries

Performance testing also covers how the API protects itself and how clients behave:

  • Rate limits: the practice API’s /rate-limit endpoint allows 5 calls per minute, then returns 429 with Retry-After and X-RateLimit-* headers.
  • Slow dependencies: /delay/{seconds} waits up to 10 seconds, ideal for testing client timeouts.
  • Client behaviour: does the app back off on 429/503, or hammer the server harder (a “retry storm”)?
GEThttps://sqatools.in/wp-json/sqa-api/v1/delay/2
curl -i \
  "https://sqatools.in/wp-json/sqa-api/v1/delay/2"
Advertisement

Quick Latency Checks Inside Your Playwright Suite

You don’t need a load tool to catch obvious regressions. A small sequential sample in your functional suite flags an endpoint that suddenly got slow:

import { test, expect } from '@playwright/test';

const BASE = 'https://sqatools.in/wp-json/sqa-api/v1/';

function percentile(values: number[], p: number) {
  const sorted = [...values].sort((a, b) => a - b);
  return sorted[Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1)];
}

test('GET users: p95 under 2 s over 15 calls', async ({ request }) => {
  const timings: number[] = [];
  for (let i = 0; i < 15; i++) {
    const start = performance.now();
    const res = await request.get(`${BASE}users?per_page=5`);
    timings.push(performance.now() - start);
    expect(res.status()).toBe(200);
  }
  const p95 = percentile(timings, 95);
  test.info().annotations.push({ type: 'p95 ms', description: p95.toFixed(0) });
  expect(p95).toBeLessThan(2_000);
});

test('client gives up on a slow endpoint', async ({ request }) => {
  const start = Date.now();
  await expect(request.get(`${BASE}delay/5`, { timeout: 2_000 })).rejects.toThrow(/Timeout/);
  expect(Date.now() - start).toBeLessThan(3_500);
});

test('rate limit answers 429 with Retry-After', async ({ request }) => {
  let last;
  for (let i = 0; i < 6; i++) last = await request.get(`${BASE}rate-limit`);
  expect(last!.status()).toBe(429);
  expect(Number(last!.headers()['retry-after'])).toBeGreaterThan(0);
});

These are not load tests (one user, sequential calls); they’re cheap guards against performance regressions that run on every build. The rate-limit test shares a per-IP counter, so don’t run it in parallel with itself.

Tools Compared

ToolScripts inStrengths
k6JavaScriptDeveloper-friendly, thresholds for CI, low resource use, cloud option
JMeterGUI / XMLMature, huge plugin ecosystem, many protocols, widely known in service companies
GatlingScala/Java/Kotlin/JSHigh performance, great HTML reports
LocustPythonEasy for Python teams, distributed
ArtilleryYAML/JSQuick configs, WebSocket and Socket.IO support
Advertisement

Common Performance Testing Mistakes

  • Reporting averages only.
  • Testing from a laptop on office Wi-Fi: the load generator becomes the bottleneck.
  • Using the same user and same product id for every request, so caches hide real costs.
  • No think time: 100 VUs without pauses behave like thousands of real users.
  • Ignoring errors: a fast 500 isn’t a fast success. Check responses in the script.
  • One run and done: compare with a baseline on every release.
Advertisement

Interview Questions: API Performance Testing

What is API performance testing?

Testing how an API behaves under various loads by measuring response time, throughput, error rate and resource usage, to verify it meets performance requirements and to find bottlenecks and limits.

What is the difference between load, stress, spike and soak testing?

Load testing uses expected peak traffic; stress testing pushes beyond capacity to find the breaking point; spike testing applies sudden large jumps; soak testing runs moderate load for a long time to reveal leaks and degradation.

Why use p95 instead of average response time?

Averages hide slow outliers. p95 shows the time within which 95% of requests complete, reflecting the experience of the slowest typical users, which is what SLAs usually target.

What is throughput?

The number of requests (or transactions) the system processes per unit of time, usually requests per second.

What are thresholds in k6?

Pass/fail criteria on metrics, such as http_req_duration p(95)<800 or http_req_failed rate<0.01. If any threshold fails, k6 exits with a non-zero code, failing the CI job.

What is think time and why is it important?

Pauses between actions that simulate real user behaviour. Without think time, a small number of virtual users generate unrealistically high request rates.

How do you identify a bottleneck from results?

When increasing load no longer increases throughput but response times and errors rise, the system is saturated. Correlate with server metrics (CPU, memory, DB connections, slow queries, downstream latency) to find the cause.

Which tools have you used for API performance testing?

Mention one or two with specifics, for example: “k6 for scripted load tests with thresholds in GitHub Actions, and JMeter for a legacy SOAP service, monitored with Grafana dashboards.”

How do you test rate limiting?

Send requests beyond the documented limit within the time window and verify 429 with Retry-After and rate limit headers, that the limit resets after the window, and that limits apply per user, key or IP as designed.

What is an SLA or SLO in performance testing?

A Service Level Agreement or Objective defining measurable targets, such as p95 latency under 800 ms and 99.9% availability, that performance tests verify.

Advertisement
View the full API testing syllabus