Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›REST API Design›Load Testing APIs

Testing

Load Testing APIs

Load testing is the process of subjecting an API to expected and peak traffic volumes to measure its performance, reliability, and responsiveness under pressure. It is crucial because it helps identify infrastructure bottlenecks, memory leaks, and concurrency issues that only manifest under heavy load. You should reach for load testing when preparing for production releases, scaling your architecture, or troubleshooting intermittent latency spikes in distributed systems.

Understanding Baseline Performance

Before attempting to stress your system, you must establish a baseline. Baseline performance refers to how your API behaves under a single, isolated request. Without knowing the 'perfect' response time—when there is no competition for CPU, memory, or network bandwidth—you cannot possibly measure the impact of concurrency. When you send one request, you are testing the efficiency of your code path, database indexing, and network overhead in a vacuum. If your baseline is already slow, adding load will only exacerbate the issue exponentially. We analyze the baseline by running a single request repeatedly to calculate the average response time. This establishes the theoretical limit of your system. Understanding this helps you distinguish between inherent latency (code overhead) and contention-induced latency (waiting for resources). Without a solid baseline, you are essentially guessing at the source of performance degradation when traffic inevitably surges in your production environment.

# Measure baseline latency for a single request
import time
import requests

def get_baseline(url, iterations=10):
    latencies = []
    for _ in range(iterations):
        start = time.time()
        requests.get(url) # Fetching resource
        latencies.append(time.time() - start)
    return sum(latencies) / iterations # Average latency

Simulating Concurrency with Threading

Once you have a baseline, the next step is to introduce concurrency. Concurrency simulates multiple users hitting your API endpoints at the exact same time. This is vastly different from sequential requests because it forces the application server and database to manage multiple execution contexts simultaneously. In a single-threaded environment, requests are queued, meaning latency is purely additive. However, in modern web servers, requests are handled in parallel, sharing finite hardware resources like file descriptors, database connections, and memory buffers. When you run concurrent tasks, you start to see performance degradation caused by context switching, lock contention on shared data, and thread starvation. By increasing the number of active threads, you move from testing logic to testing infrastructure limits. This reveals the 'saturation point' of your server, where adding one more user results in a non-linear increase in response time, often indicating that the system is exhausted and can no longer process requests efficiently.

import threading

def fire_request(url):
    # Concurrent worker to simulate a user session
    requests.get(url)

threads = [threading.Thread(target=fire_request, args=('http://api.local/data',)) for _ in range(50)]
for t in threads: t.start() # Dispatch 50 simultaneous requests
for t in threads: t.join()

Identifying Bottlenecks through Throughput

Throughput, typically measured in requests per second (RPS), is the ultimate metric for assessing how much traffic your API can handle before it fails to meet your service level objectives. As you increase the number of concurrent users, you will find that throughput eventually plateaus. This plateau is rarely arbitrary; it is usually dictated by the most constrained resource in your stack. It could be the disk I/O speed, CPU cycle availability, or a hard limit on the number of open database connections. When you observe throughput flattening despite increasing load, you have hit a bottleneck. To reason about this, consider the 'Little’s Law': the number of requests in a system is equal to the arrival rate multiplied by the time spent in the system. If the time spent increases due to resource waiting, throughput must decrease unless concurrency is increased. Finding this limit allows you to provision infrastructure precisely.

import time

# Measuring requests per second (RPS)
start_time = time.time()
for i in range(1000):
    requests.get('http://api.local/data')
total_time = time.time() - start_time
print(f"Throughput: {1000 / total_time} RPS") # Calculated capacity

Stress Testing for Failure Modes

Stress testing goes beyond normal traffic patterns to discover the point of complete system collapse. While load testing focuses on normal usage, stress testing asks, 'What happens when everything breaks?' This is vital because production environments suffer from unexpected spikes, DDoS attacks, or cascading failures where one failing service blocks others. By pushing the system until it returns status codes like 503 (Service Unavailable) or 504 (Gateway Timeout), you identify your failure modes. You want to ensure that your API fails gracefully, providing meaningful error messages rather than simply hanging or leaking sensitive stack traces. Stress testing also reveals if your circuit breakers are configured correctly—do they open when the latency exceeds the threshold, or do they remain closed and allow the system to get crushed? Understanding these failure modes ensures that your system remains resilient and recovers quickly once the underlying load is normalized or managed.

def stress_test(url, limit):
    # Keep hitting until we get a non-200 code
    for _ in range(limit):
        response = requests.get(url)
        if response.status_code >= 500:
            print(f"Failing at load: {response.status_code}")
            break

Analyzing Latency Percentiles

Relying solely on average latency is a dangerous trap because averages hide the experience of users at the extremes. If 95% of your users experience 100ms latency but 5% experience 5 seconds, your average will look acceptable, but your outlier performance is abysmal. This is why you must calculate percentiles, specifically the 95th (P95) and 99th (P99) percentiles. P99 latency tells you exactly how slow your slowest users are. When analyzing these during a load test, you will often find that the P99 latency grows much faster than the average. This disparity is usually caused by garbage collection pauses, lock contention, or slow database queries triggered by specific, non-cached requests. By monitoring these percentiles, you gain visibility into the tail latency of your system. A robust API design must aim to keep P99 latency within acceptable bounds, ensuring that even under heavy load, the experience remains consistent for all consumers.

import numpy as np

latencies = [0.1, 0.12, 0.11, 5.0] # Simulated high-load outcomes
p95 = np.percentile(latencies, 95)
p99 = np.percentile(latencies, 99)
print(f"P95: {p95}s, P99: {p99}s") # Identifying the outlier impact

Key points

  • Baseline measurements are essential to establish the performance of a single request without external contention.
  • Concurrency testing reveals how your infrastructure handles multiple simultaneous execution contexts.
  • Throughput saturation points are typically defined by the most constrained resource in your stack.
  • Stress testing helps developers understand how their application behaves during total system failure.
  • Monitoring average latency is misleading because it masks the performance degradation of outlier users.
  • P99 percentiles provide a more accurate representation of the experience for users at the extreme end of the latency distribution.
  • Little's Law demonstrates the mathematical relationship between arrival rate, latency, and system capacity.
  • Successful load testing strategies include identifying the point of failure to ensure the API degrades gracefully.

Common mistakes

  • Mistake: Testing only the 'happy path'. Why it's wrong: APIs often fail under load during complex error handling, not simple requests. Fix: Include scenarios that trigger 4xx and 5xx responses to see how the system handles error-state overhead.
  • Mistake: Running load tests from a single machine. Why it's wrong: Your local machine's bandwidth or CPU becomes a bottleneck, leading to false results about API performance. Fix: Use distributed load generators located in the same region or closer to the target infrastructure.
  • Mistake: Ignoring database state growth during tests. Why it's wrong: An API might perform well with an empty database but degrade significantly as indexes grow or tables fill up. Fix: Pre-populate the database with a realistic dataset size before initiating the load test.
  • Mistake: Focusing only on throughput (RPS). Why it's wrong: High throughput is useless if latency spikes cause timeouts for users. Fix: Monitor both throughput and latency (P95/P99) to ensure performance remains acceptable under heavy load.
  • Mistake: Not warming up the system. Why it's wrong: Cold caches and uninitialized connection pools provide inaccurate performance data. Fix: Run a 'ramp-up' period to allow caches, thread pools, and auto-scaling mechanisms to stabilize before measuring peak performance.

Interview questions

What is the primary objective of load testing a REST API?

The primary objective of load testing a REST API is to determine how the system behaves under both expected and peak traffic conditions. By simulating multiple concurrent users, we aim to measure key performance indicators such as response time, throughput, and error rates. This testing ensures that our infrastructure can handle real-world demand without degrading the user experience or causing downtime, allowing us to identify bottlenecks in database queries, network latency, or server-side resource utilization before they impact production.

Why is it important to test API endpoints with realistic data payloads during a load test?

Testing with realistic data payloads is crucial because API performance often varies based on the size and complexity of the request body. If you test only with tiny JSON objects, you might miss performance issues related to serialization, deserialization, or database insertion times for larger payloads. A realistic payload, such as a nested resource object, forces the server to process complex logic and mapping, which provides a more accurate representation of how the application handles actual usage patterns in a production environment.

Compare 'Scalability Testing' with 'Stress Testing' in the context of REST API design.

Scalability testing measures the API's ability to maintain performance as you increase the number of system resources or user demand, often validating if horizontal scaling works as expected. In contrast, stress testing pushes the API far beyond its design limits to find the 'breaking point.' While scalability testing focuses on efficiency and growth, stress testing focuses on stability and graceful failure, ensuring the API returns standard error codes like 503 Service Unavailable rather than crashing unexpectedly under extreme, unsustainable pressure.

How does the implementation of rate limiting affect the strategy for load testing an API?

Rate limiting introduces a synthetic barrier that forces the API to reject traffic once specific thresholds are met. During load testing, you must account for this; if you do not configure your test suite to handle 429 Too Many Requests responses, your test results will be skewed. A proper strategy involves testing both scenarios: one where traffic remains below the rate limit to measure latency, and one where traffic exceeds the limit to verify that the API correctly throttles requests without impacting global system performance.

Why should you monitor database connection pools during an API load test?

Monitoring database connection pools is critical because REST APIs often rely on a pool of connections to communicate with the database. Under heavy load, if the API attempts to open more connections than the pool allows, the application will experience significant latency or connection timeouts. For example, if your connection pool is set to 20 but your concurrent API requests exceed this, the requests will queue up and wait, causing a cascade effect. Observing this during a test allows you to optimize pool settings to match expected concurrent throughput.

How would you design a load test for an API endpoint that involves long-running asynchronous background processing?

Designing a test for asynchronous operations requires a two-pronged approach: measuring the initial response time for the accepted request, and polling the status endpoint until completion. You should use a webhook or polling mechanism within your load test script to track the status resource, such as 'GET /jobs/{id}'. This ensures you capture the total end-to-end latency rather than just the time it took for the server to acknowledge the initial request. This reveals if the background worker queue is becoming a bottleneck under high concurrent request volume.

All REST API Design interview questions →

Check yourself

1. Why is it important to measure the 99th percentile (P99) latency rather than just the average response time during an API load test?

  • A.Average response time is technically more difficult to calculate than P99.
  • B.P99 latency identifies the experience of the slowest users, which averages mask.
  • C.P99 latency indicates the maximum possible throughput of the API server.
  • D.Average response time is not used in modern API engineering practices.
Show answer

B. P99 latency identifies the experience of the slowest users, which averages mask.
P99 captures the outliers that affect the worst-off users, whereas averages hide spikes. Option 0 is false, 2 is incorrect as latency != throughput, and 3 is false.

2. When load testing an API that uses JWT-based authentication, which part of the process is most likely to become a CPU bottleneck?

  • A.Parsing the JSON payload in the request body.
  • B.Reading the HTTP headers of the incoming request.
  • C.Cryptographic signature verification of the JWT token.
  • D.Serializing the response object into JSON format.
Show answer

C. Cryptographic signature verification of the JWT token.
Cryptographic operations are CPU-intensive. While JSON parsing/serialization takes resources, signature verification is significantly heavier under high concurrent load. Headers are negligible.

3. What is the primary risk of using synthetic test data that does not mirror production data volume?

  • A.The API might return 404s instead of 200s due to missing IDs.
  • B.Database query execution plans may change as tables increase in size.
  • C.The load balancer will automatically block requests that look too small.
  • D.The API will ignore the request if the payload is not large enough.
Show answer

B. Database query execution plans may change as tables increase in size.
Databases optimize queries based on data distribution; small tables don't reveal performance issues inherent to scanning large tables. The other options are not inherent properties of API/DB performance.

4. If your API experiences a sudden rise in 503 Service Unavailable errors during a load test, what does this typically suggest?

  • A.The API client has provided an invalid authentication token.
  • B.The client is sending requests at a rate faster than the server can queue or process.
  • C.The database is successfully committing all transactions too quickly.
  • D.The API design does not support the HTTP GET method.
Show answer

B. The client is sending requests at a rate faster than the server can queue or process.
503 indicates the server is overloaded or down for maintenance. It is the standard response for queue saturation. The other options describe client errors or positive outcomes.

5. How does implementing a 'ramp-up' period in your load testing strategy improve result accuracy?

  • A.It forces the database to delete old logs before starting.
  • B.It prevents the load generator from crashing at the start.
  • C.It allows application caches and connection pools to reach a steady state.
  • D.It ensures that the network bandwidth is fully saturated immediately.
Show answer

C. It allows application caches and connection pools to reach a steady state.
A steady state is required to see how the system performs under sustained pressure, rather than the initial noise of startup tasks. Option 0 is irrelevant, 1 is a side effect, and 3 is not a goal.

Take the full REST API Design quiz →

← PreviousContract TestingNext →REST API Interview Questions

REST API Design

24 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app