My interest in APIs performance, metrics monitoring, and observability has been piqued ever since I read this awesome article by Shalvah. My first foray into this world which I would like to document is Load Testing.
By the end of this article, you'll have an understanding of what load testing is, why API load testing is important, practical load testing operational scenarios, and how to carry out load tests on your Django API.
What is Load Testing?
Load testing is a type of performance testing used to assess and evaluate the performance of a system under specific conditions. It may involve simulating high volume of traffic or user activities on the system.
It is a way to validate certain questions we ask, such as:
How does my application perform under a heavy traffic load?
What impact does a long-running, blocking piece of code have on the overall user experience?
In the context of this article, we want to know if the API can handle the specified amount of load and concurrent requests.
Why do you need to load test your API ?
Identify API performance bottlenecks: By simulating loads of traffic to an API during development, potential bottlenecks which may occur during production can be uncovered.
To optimise costs: Load testing your API can help you to optimise your costs by determining the minimum required resources to handle expected traffic, without over-provisioning and wasting resources.
Operational Scenarios
I’ve outlined some operational scenarios that can influence your load testing strategy for your web API. These scenarios can help you assess various aspects of your API's performance, scalability, resilience, and also gain valuable insights into how your API behaves under different conditions.
Scenario 1:
Test case: The API is accessed concurrently by 20 users, and each user performs a sequence of requests to some endpoints e,g Create a new user account, perform heavy computation, etc
Rationale: This scenario tests the API's ability to handle concurrent user sessions.
Scenario 2:
Test case: The API is subjected to a sustained traffic load of 1000 requests per second for 30 minutes, with a random payload size of up to 1 MB per request.
Rationale: This scenario tests the API's ability to handle sustained traffic loads and process large payloads, API scalability and resources utilisation.
Scenario 3:
Test case: The API caching efficiency is subjected to repeated request for same data.
Rationale: This scenario measures the impact of caching on performance and response times, hence helps in identifying any caching-related issues or optimizations that may be required.
Scenario 4:
Test case: The API receives set of requests that involve long-running operations, with an average processing time of 5 minutes per request
Rationale: This scenario allows you to evaluate how your API handles and manages long-running operations to maintain optimal performance and responsiveness for subsequent requests.
Getting started with API Load Testing Locust?
What is Locust?
Locust is an open-source, Python-based load testing tool. How do you simulate those multiple, concurrent and dynamic interactions with your application? With locust, thousands of concurrent users and connections to a website or web-based application can be simulated. User behaviour can be defined using Python code, and it includes a built-in web interface for visualising and monitoring test results in real-time.
Setting up the application
Note: The following commands and instructions provided in this article works for Linux operating systems(OS). Other OS may need modifications to the commands mentioned in this article.
The source code for this article can be gotten here
Make a directory and navigate into the directory.
mkdir django-loadtest-example && cd django-loadtest-example
Create and activate virtual environment.
virtualenv venv
source venv/bin/activate
Install the necessary packages.
pip install django locust
Create the project core directory, as well as the example application.
django-admin startproject core .
python manage.py startapp example
Add the example application to the project settings.py file
...
INSTALLED_APPS = [
...
'example',
]
...
Create urls.py file in the example app and insert the code below:
from django.urls import path
urlpatterns = [
# Define the paths here
]
Add the URL pattern for the example app in your Django core/urls.py file.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path("", include("example.urls")),
]
Run the migration and start the development server.
python manage.py migrate
python manage.py runserver
Your project should be running on your localhost with the server bound to port 8000 i.e http://127:0.0.1:8000 (or whatever port you defined).
Testing with Locust
Now that we have everything up and running, let’s start by creating and testing the endpoints.
Insert the following piece of code into the views.py file of the example app.
import time
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
@require_http_methods(["GET"])
def square_numbers(request):
squares = {num: num**2 for num in range(1, 1000001)}
return JsonResponse(squares)
@require_http_methods(["GET"])
def add_two_numbers(request):
time.sleep(5) # Illustrate a long running task being performed
a = int(request.GET.get('a', 0))
b = int(request.GET.get('b', 0))
result = a + b
return JsonResponse({'result': result})
The square_numbers view handles HTTP GET request by generating a dictionary of squares for numbers ranging from 1 to 1,000,000 and returns the result as a JSON response.
The add_two_numbers view handles HTTP GET request by simulating a long-running task with a 5-second sleep. It adds two values, a and b, extracted from the query parameters after converting them to integers. The result is then returned as a JSON response.
Create a urls.py file for the example application and paste the code that defines the path for each view in the example app's views.py file
from django.urls import path
from .views import (
square_numbers,
add_two_numbers
)
urlpatterns = [
path('square-numbers/', square_numbers, name='square_numbers'),
path('add-two-numbers/', add_two_numbers, name='load_test_endpoint'),
]
Now that the endpoints have been defined, it's time to load test them.
Create the locustfile.py in the project directory (the choice of file name is up to you), and paste the code below:
from locust import HttpUser, task, between
from random import randint
class LoadTestUser(HttpUser):
wait_time = between(1, 5)
@task
def get_square(self):
self.client.get("/square-numbers")
@task
def load_test_endpoint(self):
a = randint(0, 10)
b = randint(0, 10)
self.client.get(f'/add-two-numbers/?a={a}&b={b}')
Here is the breakdown of the snippet above:
We imported the relevant modules from the Locust package and randint from Python's random module.
The
LoadTestUser
class, nherited from Locust'sHttpUser
, was created. It represents a user or client that will be used for load testingThe
wait_time
attribute specifies the wait time between consecutive tasks executed by each user. We have set it to a random value between 1 and 5 seconds.@task
decorator specifies the tasks that will be performed during load testing. Note: The tasks are picked at random, but different weights can be given to the tasks to increase the likelihood of a specific task being picked.self.client.get()
in each function simulates the user accessing the various endpoints.
Load testing with Locust can be performed either via the Locust web interface or through the command line.
To use Locust's web interface, start the Locust server using the command:
locust -f locustfile.py
After following the instructions provided earlier, you should see a prompt in the terminal indicating that the Locust web interface is running. The web interface URL is usually http://127.0.0.1:8089 or the custom port you defined.
Once you see this message, it means the Locust web interface is up and running. You can open a web browser, navigate to the provided URL, and access the Locust dashboard to start configuring and running your load tests.
Provide the number of users, spawn rate and your server host, and click on start swarming to start the test.
For this tutorial, I specified 10 users with a spawn rate of 2 and run time of 30 seconds.The server host is the Django application server started earlier (Ensure the server is running).
Note: The spawn rate specifies the rate at which new users are added to the test every second. The idea is to simulate increase in user traffic over time.
Load Test Results
On the dashboard, you can analyse the test result such as the failure rate, RPS(request per second), etc.
If you click on the Charts tab of the Locust web interface, you will come across various metrics that provide valuable insights about your load test such as requests per second (RPS), response times and number of running users:
To carry out the above test using the command line. Use the command below
locust -f locustfile.py --headless --users 10 --spawn-rate 2 -H http://127.0.0.1:8000
Beyond this, you can explore additional features of Locust, such as distributed load testing. Read through the documentation and explore the amazing things you can carry out with Locust.
REFERENCES