Back to API Testing
Q2Question 2 of 3

How to performance test an API

Key Concepts
  • Define realistic journey scenarios that mirror real user behavior, not isolated endpoint hammering
  • Set up production-like test data and environment because query performance depends on table sizes and indexes
  • Ramp virtual users in steps (100, 500, 1000), holding at each level long enough for stable measurements
  • Measure response-time percentiles (p95, p99), throughput (req/sec), and error rate at each load level
  • The breaking point is where latency climbs sharply, error rate rises, or throughput plateaus or falls
  • Diagnose the bottleneck by correlating client-side degradation with server-side resource metrics, then fix and re-test

Definition

Define realistic journey scenarios, set up test data and a production-like environment, ramp virtual users 100 to 500 to 1000 while measuring latency percentiles and error rate, find the breaking point, diagnose and fix the bottleneck via server metrics, then re-test.
  • Scenario design – Define realistic end-to-end journeys that mirror real user behavior with correlated calls and think times.
  • Data and environment – Seed production-like data and run against a production-like environment because performance is sensitive to both.
  • Stepped ramping – Increase virtual users in steps (100, 500, 1000), holding at each level for stable measurements.
  • Key metrics – Measure response-time percentiles (p95, p99), throughput (requests per second), and error rate at each level.
  • Breaking point – The load at which latency climbs sharply, error rate rises, or throughput plateaus or falls.
  • Diagnose and fix – Correlate client-side degradation with server-side resource metrics (CPU, DB connections, slow queries), fix the bottleneck, and re-test.

To performance test the National Bank of India's API, you first write down the real journeys a customer takes: log in, view accounts, make a transfer. You prepare the practice accounts and point at an environment that looks like the real one. Then you simulate a growing crowd: a hundred users, then five hundred, then a thousand, timing how the API responds and how many requests fail. You keep increasing until the API starts to buckle, find the breaking point, look inside to see what gave out first, fix that bottleneck, and run the test again to prove it is faster.

Understanding the Concepts

How do you define realistic scenarios?

Real load is a mix of correlated calls, not isolated endpoint hammering. A banking scenario includes login, fetch accounts, view a statement, and perform a transfer, with realistic think times between steps and a request mix weighted like production.

JAVASCRIPT
// k6 scenario: realistic banking journey
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 100 },   // Ramp to 100 users
    { duration: '5m', target: 100 },   // Hold at 100
    { duration: '2m', target: 500 },   // Ramp to 500
    { duration: '5m', target: 500 },   // Hold at 500
    { duration: '2m', target: 1000 },  // Ramp to 1000
    { duration: '5m', target: 1000 },  // Hold at 1000
    { duration: '2m', target: 0 },     // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p95<200'],    // p95 under 200ms
    http_req_failed: ['rate<0.01'],    // Error rate under 1%
  },
};

export default function () {
  // Step 1: Login
  const loginRes = http.post(
    `${__ENV.BASE_URL}/api/auth/login`,
    JSON.stringify({ username: `user-${__VU}`, password: 'test' }),
    { headers: { 'Content-Type': 'application/json' } }
  );
  check(loginRes, { 'login 200': (r) => r.status === 200 });
  const token = loginRes.json('token');
  sleep(1); // Think time

  // Step 2: Fetch accounts
  const accountsRes = http.get(
    `${__ENV.BASE_URL}/api/accounts`,
    { headers: { Authorization: `Bearer ${token}` } }
  );
  check(accountsRes, { 'accounts 200': (r) => r.status === 200 });
  sleep(2);

  // Step 3: Transfer
  const transferRes = http.post(
    `${__ENV.BASE_URL}/api/transfer`,
    JSON.stringify({
      fromAccount: 'ACC-001',
      toAccount: 'ACC-002',
      amount: 1000,
    }),
    { headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    }}
  );
  check(transferRes, { 'transfer 200': (r) => r.status === 200 });
  sleep(1);
}

How do you set up data and environment?

Seed enough accounts and users for the planned concurrency with production-like data volume. Isolate data so virtual users do not collide.

BASH
# Seed test data for 1000 virtual users
for i in $(seq 1 1000); do
  curl -X POST "$BASE_URL/api/test/accounts" \
    -H 'Content-Type: application/json' \
    -d "{\"id\": \"ACC-$i\", \"balance\": 100000}"
done

How do you interpret the results?

At each load level, measure p95 and p99 latency, throughput, and error rate. Compare against the SLA.

DIAGRAM
  Load Level │ p95 (ms) │ p99 (ms) │ Throughput │ Error Rate
  ───────────┼──────────┼──────────┼────────────┼──────────
  100 users  │   45     │   80     │ 850 req/s  │  0.0%
  500 users  │   120    │   250    │ 3200 req/s │  0.1%
  1000 users │   380    │   1200   │ 3100 req/s │  2.5%
  ───────────┼──────────┼──────────┼────────────┼──────────
  Breaking point: ~800 users (throughput plateaus, p99 spikes)

How do you diagnose the bottleneck?

Correlate client-side degradation with server-side resource metrics.

TEXT
  At 1000 users:
  - DB connection pool: 100% utilized (bottleneck!)
  - CPU: 45% (not the limit)
  - Memory: 60% (not the limit)
  - Slow query: SELECT * FROM transactions (no index on account_id)

  Fix:
  1. Increase connection pool from 20 to 50
  2. Add index: CREATE INDEX idx_txn_account ON transactions(account_id)
  3. Re-test
JAVA
// Common bottleneck: N+1 query pattern
// Before: 1001 queries for 1000 accounts
for (Account acc : accounts) {
    List<Transaction> txns = repo.findByAccountId(acc.getId());
}

// After: 1 query with JOIN
List<AccountWithTransactions> all =
    repo.findAllWithTransactions();

How do you run this in CI/CD?

Integrate k6 with thresholds that fail the pipeline on a performance regression.

YAML
# .github/workflows/perf.yml
jobs:
  performance:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run k6 load test
        run: |
          k6 run --env BASE_URL=${{ vars.STAGING_URL }} \
            perf/transfer-journey.js
        # k6 exits non-zero if thresholds fail

What is the iterative loop?

Performance testing is not a one-shot check. It is an iterative loop: load, measure, find the limit, fix, re-test.

DIAGRAM
  ┌─────────────────┐
  │ Define scenarios │
  └────────┬────────┘
           │
  ┌────────▼────────┐
  │ Run with ramp   │ 100 -> 500 -> 1000
  └────────┬────────┘
           │
  ┌────────▼────────┐
  │ Measure metrics  │ p95, p99, throughput, errors
  └────────┬────────┘
           │
  ┌────────▼────────┐
  │ Find breaking   │ Where degradation starts
  │ point           │
  └────────┬────────┘
           │
  ┌────────▼────────┐
  │ Diagnose with   │ Server CPU, DB, pool, queries
  │ server metrics  │
  └────────┬────────┘
           │
  ┌────────▼────────┐
  │ Fix bottleneck  │ Pool size, index, cache
  └────────┬────────┘
           │
           └──> Re-test (loop until targets met)

What are common interview follow-up questions?

  • "Why not just test a single endpoint?" Because real load is correlated calls with dependencies. A transfer depends on a session from login and an account from the accounts call.
  • "How do you know when to stop increasing users?" When latency or error rate exceeds your SLA, or when throughput stops growing.
  • "What if the test environment is smaller than production?" Scale the results proportionally, but acknowledge the uncertainty. Production-like is always better.

What are the common bottlenecks?

Common API bottlenecks include database connection pool too small, unindexed queries, insufficient CPU or instances, slow downstream services, and lock contention.

JAVA
// Common bottleneck: connection pool exhaustion
// Before: pool size 20, 1000 concurrent requests
// Result: requests queue, timeouts spike
// Fix: increase pool to 50, add connection timeout

// Common bottleneck: N+1 query
// Before: 1001 queries for 1000 accounts
for (Account acc : accounts) {
    List<Transaction> txns = repo.findByAccountId(acc.getId());
}
// After: 1 query with JOIN
List<AccountWithTransactions> all = repo.findAllWithTransactions();

Visual Overview

DIAGRAM
  Performance Testing Loop
  ────────────────────────

  Define ──> Set up data ──> Ramp ──> Measure ──> Breaking point?
    │                                               │
    │                        No ◄───────────────────┘
    │                         │
    │                  ┌──────▼──────┐
    │                  │ Diagnose    │
    │                  │ bottleneck  │
    │                  └──────┬──────┘
    │                         │
    │                  ┌──────▼──────┐
    │                  │ Fix         │
    │                  └──────┬──────┘
    │                         │
    └─────────────────────────┘ Re-test

Key Takeaway

I performance test an API as a structured, iterative loop. First I define scenarios that mirror real user journeys with correlated calls and think times, not isolated endpoint hammering. Second I set up production-like data and environment. Third I execute a ramping workload in steps: 100, 500, 1000 users, holding at each level for stable readings. At each level I measure p95 and p99 latency, throughput, and error rate against the SLA.

The crucial step is diagnosing the bottleneck by correlating client-side degradation with server-side metrics: is CPU saturated, is the connection pool exhausted, is a query slow? Then I fix the identified bottleneck and re-test until the targets are met.

At the National Bank of India I did exactly this for the transfer journey ahead of salary day. Ramping with k6 I found the API broke around 800 concurrent users with latency spiking, and server metrics showed the database connection pool was saturated and one query was unindexed. I enlarged the pool and added the index, re-ran the same ramp, and the breaking point moved well past our expected peak.

Remember one sentence: define realistic journeys, ramp in steps, measure percentiles and error rate, find the breaking point, diagnose the bottleneck from server metrics, fix it, and re-test until the targets are met.

---

Share
Use ← → arrow keys to navigate