API Performance Testing

API performance testing measures how each endpoint of an API responds under a defined load: response time percentiles, throughput, error rate and the size of what it sends and receives. You call the endpoints directly with a load generator, without the user interface. Example: a shop's mobile app calls GET /products/{id} 150 times per second at peak; the test checks that p95 stays at or under 200 ms, with less than 0.1 % errors, at that rate.

It applies the vocabulary of this module (percentiles, workload models, load tests) to single endpoints and answers three questions: what target each endpoint must meet, how payload size affects it, and how it behaves when many requests arrive at once.

Why it matters

Every app screen and partner integration is a chain of API calls, so one slow endpoint slows every screen that calls it, and in sequential calls the delays add up. Problems such as an unbounded list or a lock on a popular row stay invisible in functional tests, which send one request at a time with little data.

For the tester, endpoint targets turn "the app feels slow" into a reportable result: which endpoint, which percentile, at what load, against which target. API-level tests can also start as soon as an endpoint exists, long before a full end-to-end load test.

Targets per endpoint, not per API

Endpoints do very different work: a health check returns a constant, a lookup reads one row, a search filters thousands of records, and creating an order writes to several tables and may call a payment provider. One target for "the API", such as "everything under 500 ms", hides regressions in the cheap endpoints and is unreachable for the heavy ones.

A testable target names the endpoint and method, the percentile (p95, often also p99), the threshold, the load at which it must hold, the acceptable error rate and the data condition, such as the page size. Without a load level, "p95 under 200 ms" can neither pass nor fail. Targets come from non-functional requirements or SLOs, from the client's needs (a screen that calls two endpoints in sequence must split its time budget) and from production monitoring. The table is one team's example, not a standard.

Slow work such as a large export should not hold the request open. A common design answers 202 Accepted at once (RFC 9110: accepted for processing, but not completed) and lets the client fetch the result later; you then test two targets, the time to accept and the time until the result is ready.

Shop API: targets at the expected peak (an example agreed by one team)

Endpoint                      Load        p95       p99       Errors
GET  /health                  20 req/s    <= 50 ms  <= 100 ms 0 %
GET  /products/{id}           150 req/s   <= 200 ms <= 400 ms < 0.1 %
GET  /products?q=...&limit=20 60 req/s    <= 500 ms <= 1 s    < 0.1 %
POST /orders                  10 req/s    <= 800 ms <= 1.5 s  < 0.1 %
POST /reports/export          1 req/min   202 Accepted <= 300 ms; file ready <= 60 s

Try it: Response Time Analyzer Try it: API Performance Comparator

Payload size is part of the response time

The response time a client sees includes building, sending and parsing the body. The same endpoint can be fast with 20 items and slow with 2,000, quick on the office network and slow on a mobile connection. Test each endpoint at the sizes it will really send and receive.

  • Measure the body raw and as transferred: when the client sends Accept-Encoding: gzip, does the response carry Content-Encoding, and how many bytes does it save?
  • Lists must be bounded: test the default page size, the documented maximum and a value above it. limit=100000 should be capped or rejected, not served.
  • Grow the data: call the same endpoint for a customer with 5 orders and one with 5,000. Time that grows with the data points to missing pagination or a query per item.
  • Apply boundary value analysis to request bodies: time a batch endpoint with 1 item, a typical number and the documented maximum, then send one item above the maximum and expect a fast, documented 4xx, not a slow timeout. For a body over the server's size limit, RFC 9110 defines 413 Content Too Large.

Try it: Response Size Analyzer Try it: Large JSON Dataset Generator

Concurrency: one fast request proves little

A functional API test sends one request at a time, so it shows the best case. Under concurrency, requests compete for worker threads, database connections, locks and downstream calls, and an endpoint that answers in 80 ms alone may queue for seconds. Using the workload models from the earlier lessons, step the load: a single-user baseline, the expected peak (the busiest traffic forecast from analytics or by the business), then above it, which is stress testing. Record p95, p99, throughput and error rate at each step.

Where the requests land matters too. Requests spread over many records test the normal path; many requests on one record, such as a discounted product in a sale, test locking. Design both on purpose: varied test data for the first, one "hot" record for the second.

  • Check correctness, not only timing: assert the status and key fields of every response, and afterwards check side effects, such as no oversold stock and no order created twice when a client retried.
  • Separate expected refusals from failures: above a documented rate limit expect 429 Too Many Requests (RFC 6585), and count it apart from 5xx errors and timeouts such as 504 Gateway Timeout.

Try it: Concurrent Request Test Generator

Worked example: a shop API before a sale

A shop prepares for a sale weekend. The tester runs the product, search and order endpoints from the table at their peak rates for 15 minutes in a staging environment sized like production, with 20,000 product ids and 2,000 test customers, from load generators on separate machines. The results, as written in the report:

  • GET /products/{id}: p95 140 ms, p99 260 ms, no errors. Within target.
  • GET /products?q=... with the default limit=20: p95 430 ms, within target. The web app's "show all" button sends limit=200: 1.8 MB uncompressed and p95 1,900 ms. Finding: no maximum page size.
  • POST /orders at 10 req/s spread over many products: p95 650 ms, within target. When 50 virtual users order one discounted product at once, p95 rises to 1,400 ms and 2 % of requests end in 504 Gateway Timeout. Finding: requests queue on that product's stock update.
  • Afterwards the stock levels match the orders: nothing oversold, nothing duplicated. The concurrency problem is slowness, not data corruption.
Measured p95 against each endpoint's own target: lookup passes at 140 ms, search passes at limit=20 but takes 1,900 ms at limit=200, and orders reach 1,400 ms when 50 users buy one product.

GET /products/{id} at 150 req/s: p95 target 200 ms

Axis from 0 ms to 400 ms
  • ≤ 200 msWithin target
  • > 200 msOver target
  1. 1140 ms Measured p95, varied idsNo errors; p99 260 ms.Pass

GET /products?q=... at 60 req/s: p95 target 500 ms

Axis from 0 ms to 2000 ms
  • ≤ 500 msWithin target
  • > 500 msOver target
  1. 1430 ms limit=20, the default pagePass
  2. 21900 ms limit=200, the "show all" button1.8 MB uncompressed: no maximum page size.Fail

POST /orders at 10 req/s: p95 target 800 ms

Axis from 0 ms to 1600 ms
  • ≤ 800 msWithin target
  • > 800 msOver target
  1. 1650 ms Orders spread over many productsPass
  2. 21400 ms 50 users order one product at onceQueueing on one stock row; 2 % end in 504.Fail

Targets are the example team's agreement, not an industry standard; each axis has its own range because the targets differ.

Tools: from one request to a scripted load test

Small tools cover the steps around a load test. Mutqan's Response Time Analyzer gives a single-user baseline for one endpoint from up to 20 sequential requests (min, max, average, median and p95); the Response Size Analyzer shows, for a URL or a pasted body, the raw bytes, an estimated compressed size and the largest fields; the API Performance Comparator puts two endpoints side by side; the HTTP Request Benchmark runs a small, bounded benchmark. These tools send their requests from Mutqan's server, so they reach only public URLs and their timings include the network path from that server. None of them generates production-scale load.

For that you need a load generator. k6 runs JavaScript scripts whose thresholds fail the run when a target is missed; Apache JMeter builds test plans in a GUI, reads test data with CSV Data Set Config and should run in non-GUI mode for the real test; Gatling and Locust (Python) are other common choices. The k6 script below encodes the product-lookup target. Load test only systems you own or have written permission to test: to a third-party provider, an unannounced load test looks like a denial-of-service attack.

// Run: k6 run -e BASE_URL=https://staging.example.com product-lookup.js
import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';

// 20,000 product ids: varied data, so requests do not all hit one cached record
const ids = new SharedArray('ids', () =>
  open('./product-ids.txt').split(/\r?\n/).filter(Boolean));

export const options = {
  scenarios: {
    product_lookup: {
      executor: 'constant-arrival-rate', // open model: a fixed rate of new requests
      rate: 150,
      timeUnit: '1s',
      duration: '15m',
      preAllocatedVUs: 50,
      maxVUs: 300,
    },
  },
  thresholds: {
    'http_req_duration{endpoint:product}': ['p(95)<=200', 'p(99)<=400'],
    'http_req_failed{endpoint:product}': ['rate<0.001'],
  },
};

export default function () {
  const id = ids[Math.floor(Math.random() * ids.length)];
  const res = http.get(`${__ENV.BASE_URL}/products/${id}`, {
    tags: { endpoint: 'product' },
  });
  check(res, {
    'status is 200': (r) => r.status === 200,
    'body has a price': (r) => r.status === 200 && r.json('price') !== undefined,
  });
}

Try it: HTTP Request Benchmark Try it: Concurrent Request Test Generator Try it: Load Test Data Generator

Common mistakes

  • One target for the whole API: too loose for the cheap endpoints, impossible for the heavy ones.
  • A target without its load and data condition: "p95 under 200 ms", but at what request rate and page size?
  • Testing with one id and tiny data sets, so every request is answered from a cache.
  • Measuring only time: fast 500 or 429 responses make latency look good. Assert status and body.
  • Ignoring payload size because the test runs next to the server, where even a 2 MB response looks fast.
  • Running the load generator on the API's own machine, or load testing a third-party or production API without permission.

The QA perspective

Start in design reviews. For each new endpoint, ask who calls it and in what order, the expected peak, the maximum page size, whether heavy work is asynchronous and what the rate limit is. Agree the targets with the product owner, the developers and operations, and write them next to the endpoint's functional acceptance criteria.

For each run, record the build, environment, dataset, load profile and, per endpoint, p95, p99, throughput, errors by status code and payload size against the target. Write each finding so that someone can act on it: the endpoint, the load level, the percentile measured against its target, the error type (such as 504 timeouts), the saturated resource (the one that ran out first, such as database connections or a lock on one row) with the server metrics that show it, and the re-run that confirms the fix. Keep a small threshold check for the key endpoints in the pipeline, so a regression shows up on the commit that caused it.

Practice exercise

Plan the performance tests for a clinic booking API

Your task

Using the material below, write a target for each endpoint of a clinic booking API, with its load and data condition, identify the payload-size and concurrency risks, and choose the tests and tools. Your targets are a proposal for the team to agree.

Material
Clinic booking API: expected peak, Sunday 08:00 to 09:00

GET  /clinics/{id}/slots?date=   40 req/s   all slots of one clinic for one day
POST /appointments                5 req/s   books one slot; at 08:00 up to 200
                                            patients try to book the same slot
GET  /appointments?patientId=     8 req/s   visit history; some patients have 300+
GET  /reports/daily               rare      builds a PDF of all bookings of a clinic

Mobile "Book" screen: calls slots, then appointments, in sequence; budget 1.5 s
Steps
  1. Write one target row per endpoint, with its load level and data condition.
  2. Split the screen's 1.5 s budget between its two sequential calls.
  3. List the payload-size and concurrency tests, including the popular slot.
  4. Name the checks beyond timing and the tools for the baseline and the load run.
Show the model answer
Model answer

Example targets (a proposal, not a standard). Slots: p95 300 ms and p99 600 ms at 40 req/s, on the busiest day. Appointments: p95 600 ms and p99 1 s at 5 req/s, errors under 0.1 %, not counting the expected conflict response for a taken slot. History: paginated, p95 400 ms at 8 req/s for a page of 20, for a patient with 300+ visits. Report: 202 Accepted within 300 ms and the PDF ready within 60 s; if it is built inside the request, raise a design question.

Budget: 300 ms plus 600 ms leaves about 600 ms of the 1.5 s for the mobile network and rendering. Adding two p95 targets is a planning approximation, not the screen's exact p95, so also measure the screen end to end.

Payload and concurrency: measure the busiest day's slots response raw and compressed, and test history page sizes up to and above the maximum. Send 200 simultaneous bookings for one slot: expect exactly one success (such as 201 Created), the documented conflict (such as 409 Conflict) for the rest, no double booking afterwards and p99 within target; repeat with bookings spread over many slots.

Tools: Response Time Analyzer and Response Size Analyzer for baselines if the test environment has a public URL (otherwise paste a saved response into the Response Size Analyzer), Load Test Data Generator for patient ids and dates, and Concurrent Request Test Generator for a starting k6 script per endpoint, whose p95 and error-rate thresholds you set to your targets. Run the load in the test environment only, with the team's agreement.

Frequently asked questions

Is API performance testing the same as load testing?

No. Load testing is a test type: the system at its expected load. API performance testing says where you measure: at the API, calling the endpoints directly without a user interface. Against them you can run load, stress, spike or soak tests. This isolates the server from browser rendering and gives every endpoint its own target.

Where do I get targets if nobody gives me any?

From evidence: current p95 and p99 per endpoint from production monitoring, the time budget of the screens or integrations that call the endpoint, and the expected peak. Propose numbers, agree them with the product owner and developers, and record them as non-functional requirements. Do not copy numbers from articles about other systems.

Can I load test an API with an online tool?

Not at realistic scale. Online tools, including Mutqan's performance tools, suit baselines, comparisons, payload checks and small, bounded benchmarks. Realistic concurrency needs a load generator such as k6, JMeter, Gatling or Locust, on machines separate from the system under test, against an environment you are allowed to load.

References

Practise with Mutqan tools

Measure an endpoint's response time over several samples: min, max, average, median and p95.

Performance

Analyze response size: raw vs compressed bytes, headers size, JSON structure weight and largest fields.

Performance

Run a small, safe benchmark against a public endpoint (bounded requests and concurrency) and get throughput and latency percentiles.

Performance

Compare response times and sizes of two endpoints (e.g. v1 vs v2, staging vs production) side by side.

Performance

Generate concurrency test scripts (k6, JMeter JMX, Locust, Artillery, Node.js) with safe defaults for a target endpoint.

Automation

Generate large parameter datasets (users, tokens, ids, payloads) for k6, JMeter, Gatling and Locust load tests.

Performance

Go deeper

Your progress on this topic

Part of the QA Roadmap