Forum Diskusi dan Komunitas Online

Full Version: API Performance Testing: Metrics, Methods, and Best Practices
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
[attachment=9652]

API Performance Testing: Metrics, Methods, and Best Practices
A partner's engineer once sent me a screenshot of her team's error logs and one polite sentence: "Your API keeps timing out for us."

Our dashboard said the opposite. Average response time was under 200 milliseconds, error rate was low, everything was green. We were both looking at accurate data. The difference was that she was calling an endpoint with large result sets, a few times a minute, and those calls were landing in the slowest one percent of our traffic. Averaged in with millions of small, quick requests, her experience was invisible.

I've changed some details, but the pattern is one of the most common I see. The API wasn't broken for most callers, so the numbers looked healthy. And the one caller whose business depended on the slow path had no way to prove it, because nobody was measuring what she cared about.

That's what this article is about. API performance testing is less about running a load tool and more about choosing numbers that can't hide a problem, then generating traffic that actually looks like your callers. I'll start with the numbers.
[attachment=9651]

Six Numbers Worth Putting on the Wall
Latency, as a distribution
Report percentiles, not averages: the median (p50) for the typical call, p95 and p99 for the unlucky ones. Better still, keep a histogram, so you can see the shape of the distribution. A long tail is a different problem from a uniformly slow service.

Two cautions. First, you can't average percentiles. If three servers report p99 values, the p99 of the combined traffic isn't their mean. Aggregate the underlying histograms or raw samples. Second, break latency down by endpoint and by request class. One blended number for "the API" is how a partner with large responses gets lost.

Error rate, split properly
A single "error rate" hides the interesting part. Separate client errors (4xx), server errors (5xx), timeouts, and throttling responses (429). They have different causes and different owners. And count timeouts as failures even if the server eventually finished the work, because from the caller's side, they were.

There's a trap worth naming here: fast failures flatter your latency. A request that returns a 500 in five milliseconds improves your percentiles. If a system under stress starts failing quickly, the latency graph may actually improve while the service falls apart. Always read latency of successful requests next to the error rate, never alone.

Throughput that comes with context
Requests per second means little by itself. What matters is successful requests per second at an acceptable latency, with a realistic mix of endpoints. A system that sustains a big number by serving cheap requests tells you nothing about its capacity for expensive ones.

Saturation on the server side
Latency tells you that something is slow. Saturation helps explain why. Watch connection pool usage, thread pool occupancy, queue depth, CPU, memory and database connections. Average utilization can be misleading, since a pool that's full for two seconds out of every ten will look fine on a one-minute average and ruin the tail.

Time to first byte and payload size
For APIs that return large responses, or stream, the time until the first byte arrives and the time to transfer the rest are two different stories. A tiny test payload can hide the fact that the real responses are hundreds of kilobytes. Track response size alongside latency, and test with realistic data volumes.

Workflow success
Callers rarely use one endpoint. They authenticate, create something, poll for status, and fetch results. Each endpoint can meet its target while the whole sequence fails its budget, so measure the end-to-end journey too.

Three Numbers I'd Quietly Retire
Average latency as a headline. Fine as a rough reference, dangerous as the main number.

Maximum latency on its own. One slow outlier makes it jump around, and it's too noisy to act on. Use high percentiles instead.

Average CPU as proof of health. Services can be starved on threads, connections or locks while the CPU sits comfortably idle.

The Arithmetic of Tail Latency
Here's a small calculation that changes how people think about p99. Suppose a request fans out across five sequential service calls, and each one has a one percent chance of being slow (it's in its own p99). If the calls are independent, the chance that at least one is slow is:

1 minus 0.99 to the fifth power, which is about 4.9 percent.

So a request that touches five services meets "a p99 event" roughly once in twenty times, not once in a hundred. That's an idealized model, since real calls aren't perfectly independent, but the direction holds, and it explains why microservice architectures and AI agents that chain many calls are so sensitive to tail latency. It's also a good argument for setting per-hop budgets that are tighter than the end-to-end one.

What a Realistic Workflow Test Looks Like
Here's a short example in k6 that models arrivals at a steady rate, authenticates once, creates and fetches an order, and applies separate targets per endpoint. The numbers are placeholders to show the structure, not recommendations.

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

export const options = {
  scenarios: {
    orders: {
      executor: 'constant-arrival-rate',
      rate: 40, timeUnit: '1s', duration: '15m',
      preAllocatedVUs: 80, maxVUs: 300,
    },
  },
  thresholds: {
    'http_req_duration{endpoint:create_order}': ['p(95)<600'],
    'http_req_duration{endpoint:get_order}':    ['p(95)<250'],
    http_req_failed: ['rate<0.01'],
  },
};

export function setup() {
  const res = http.post(`${__ENV.BASE}/auth/token`, JSON.stringify({ client: 'perf-test' }),
    { headers: { 'Content-Type': 'application/json' } });
  return { token: res.json('access_token') };
}

export default function (data) {
  const headers = { Authorization: `Bearer ${data.token}`, 'Content-Type': 'application/json' };
  const body = JSON.stringify({ sku: `SKU-${Math.floor(Math.random() * 5000)}`, qty: 1 });

  const created = http.post(`${__ENV.BASE}/orders`, body, { headers, tags: { endpoint: 'create_order' } });
  check(created, { 'created': ® => r.status === 201 });

  const id = created.json('id');
  const fetched = http.get(`${__ENV.BASE}/orders/${id}`, { headers, tags: { endpoint: 'get_order' } });
  check(fetched, { 'found': ® => r.status === 200 });
}


A few choices in that script are deliberate, and each one reflects a mistake I've seen.

It uses an arrival rate, not a fixed pool of waiting users. Many load tools, in their default mode, wait for each response before sending the next request. When the API slows down, the tool automatically sends less traffic, and the tail latency looks better than a real caller would experience. This effect is sometimes called coordinated omission, and an arrival-rate model avoids most of it.

It logs in once, not on every iteration. Unless authentication is part of the load you're studying, hammering the identity provider tests the wrong system, and may get your test blocked.

It varies the payload. Identical requests hit caches that real traffic wouldn't, and flatter the result.

It tags each endpoint, so you get percentiles per endpoint, not one blended figure.

A Checklist for Tests That Hold Up
  • Percentile targets are agreed per endpoint before the run, and not adjusted after seeing results
  • Latency of successful requests is reported next to error and timeout rate
  • The workload mix comes from production data, not guesses
  • Test data is varied, large enough to avoid cache artifacts, and isolated per run
  • The load generator's own CPU, memory and network are monitored, so you're not measuring the tool
  • Server-side saturation metrics are captured, not just client-side timings
  • Downstream dependencies are stubbed with realistic latency and failure behavior
  • Results are compared against a stored baseline from the previous release
  • A small version of the test runs in CI, and the full version on a schedule

Enterprise Realities
The gateway is part of the system. API gateways add latency, enforce rate limits, and often terminate TLS and authentication. Test through the gateway, not around it, and know its limits.

Consumers aren't equal. Large partners, internal services and free-tier users may deserve different service levels. Test fairness: does one heavy tenant degrade everyone else?

Versions multiply the surface. If you support several API versions, performance can differ between them. Include the ones with real traffic.

Shared test environments distort results. If other teams are loading the same cluster, your numbers are contaminated. Prefer isolated or ephemeral environments, and record the differences from production.

Production traffic needs care. Replaying real requests means handling personal data, tokens and side effects. Scrub, tokenize, and make sure replays can't trigger real payments or emails.

Monitoring is the continuation of testing. The same percentile and error metrics should exist in production, with alerts tied to the objectives you tested against. Synthetic probes of key workflows catch regressions between releases.

When the API Has a Model Behind It
If your API fronts an LLM, a retrieval pipeline or an agent, a few of the metrics change shape.

Total response time is less informative than time to first token and tokens per second, particularly for streaming responses. Latency depends heavily on prompt length and output length, so test with a realistic spread of both. Watch queue wait and 429 responses from model providers, since your effective capacity may be set by a rate limit you don't control. Track cost per request during the run, because a load test can be an expensive way to learn something. And for agents that make several calls per request, the tail arithmetic above applies in full: each additional hop adds exposure.

AI can help with the work around the test, such as generating varied realistic prompts, clustering slow traces, or flagging anomalies in results. The pass or fail decision should still come from agreed numerical targets, not from a model's impression of the graphs.

Where Teams Trip
A few patterns keep showing up. People test a single happy-path endpoint and call the API ready. They measure from a load generator in the same data center as the service and ignore network distance real callers experience. They hammer an API with one valid token and one valid record, so everything is cached. They forget that rate limits and quotas change behavior under load, and so measure the throttling instead of the service. And they treat the first green run as permanent, when API performance drifts with every release.

None of these are exotic. They're just easy to do when you're in a hurry.

A Closing Thought
Try this on Monday. Pick your most important API and ask for its p99 latency, broken down by endpoint and by consumer, for the last week. If the answer takes more than a few minutes to produce, your monitoring has a gap, and the first thing to fix is the ability to ask the question. If it's quick and the numbers surprise you, you've found where to start testing.

I expect API performance practice to move toward contracts that include latency and error budgets alongside the schema, checked automatically for every consumer. Teams that treat "how fast, for whom, and how reliably" as part of the interface will have fewer arguments and fewer screenshots from unhappy partners. If you'd like help building that kind of measurement habit, our API Performance Testing practice at PrimeQA Solutions works with engineering and product teams on exactly this.