Databases power the backbone of modern applications, and PostgreSQL is one of the most powerful open-source relational databases trusted by top companies worldwide. Whether youβre a beginner or a developer looking to sharpen your database skills, this FREE bootcamp will take you from Zero to Hero in PostgreSQL!
This intensive hands on bootcamp is designed for developers, DBAs, and tech enthusiasts who want to master PostgreSQL from scratch and apply it in real-world scenarios.
Who Should Attend?
Beginners eager to learn databases Developers & Engineers working with PostgreSQL Anyone looking to optimize their SQL skills
Date: March 22, 23 Time: Will be finalized later. Location: Online Cost:100% FREE
Spike testing is a type of performance testing that evaluates how a system responds to sudden, extreme increases in load. Unlike stress testing, which gradually increases the load, spike testing simulates abrupt surges in traffic to identify system vulnerabilities, such as crashes, slow response times, and resource exhaustion.
In this blog, we will explore spike testing in detail, covering its importance, methodology, and full implementation using K6.
Why Perform Spike Testing?
Spike testing helps you
Determine system stability under unexpected traffic surges.
Identify bottlenecks that arise due to rapid load increases.
Assess auto-scaling capabilities of cloud-based infrastructures.
Measure response time degradation during high-demand spikes.
Ensure system recovery after the sudden load disappears.
http_req_duration β Measures response time impact.
vus_max β Peak virtual users during the spike.
errors β Percentage of failed requests due to overload.
Best Practices for Spike Testing
Monitor application logs and database performance during the test.
Use auto-scaling mechanisms for cloud-based environments.
Combine spike tests with stress testing for better insights.
Analyze error rates and recovery time to ensure system stability.
Spike testing is crucial for ensuring application stability under sudden, unpredictable traffic surges. Using K6, we can simulate spikes in both requests per second and concurrent users to identify bottlenecks before they impact real users.
Stress testing is a critical aspect of performance testing that evaluates how a system performs under extreme loads. Unlike load testing, which simulates expected user traffic, stress testing pushes a system beyond its limits to identify breaking points and measure recovery capabilities.
In this blog, we will explore stress testing using K6, an open-source load testing tool, with detailed explanations and full examples to help you implement stress testing effectively.
Why Stress Testing?
Stress testing helps you
Identify the maximum capacity of your system.
Detect potential failures and bottlenecks.
Measure system stability and recovery under high loads.
Ensure infrastructure can handle unexpected spikes in traffic.
K6 provides various executors to simulate different traffic patterns. For stress testing, we mainly use
ramping-vus β Gradually increases virtual users to a high level.
constant-vus β Maintains a fixed high number of virtual users.
spike β Simulates a sudden surge in traffic.
Example 1: Basic Stress Test with Ramping VUs
This script gradually increases the number of virtual users, holds a peak load, and then reduces it.
import http from 'k6/http';
import { sleep } from 'k6';
export let options = {
stages: [
{ duration: '1m', target: 100 }, // Ramp up to 100 users in 1 min
{ duration: '3m', target: 100 }, // Stay at 100 users for 3 min
{ duration: '1m', target: 0 }, // Ramp down to 0 users
],
};
export default function () {
let res = http.get('https://test-api.example.com');
sleep(1);
}
Explanation
The test starts with 0 users and ramps up to 100 users in 1 minute.
Holds 100 users for 3 minutes.
Gradually reduces load to 0 users.
The sleep(1) function helps simulate real user behavior between requests.
Example 2: Constant High Load Test
This test maintains a consistently high number of virtual users.
import http from 'k6/http';
import { sleep } from 'k6';
export let options = {
vus: 200, // 200 virtual users
duration: '5m', // Run the test for 5 minutes
};
export default function () {
http.get('https://test-api.example.com');
sleep(1);
}
Explanation
200 virtual users are constantly hitting the endpoint for 5 minutes.
Helps evaluate system performance under sustained high traffic.
Example 3: Spike Testing (Sudden Traffic Surge)
This test simulates a sudden spike in traffic, followed by a drop.
import http from 'k6/http';
import { sleep } from 'k6';
export let options = {
stages: [
{ duration: '10s', target: 10 }, // Start with 10 users
{ duration: '10s', target: 500 }, // Spike to 500 users
{ duration: '10s', target: 10 }, // Drop back to 10 users
],
};
export default function () {
http.get('https://test-api.example.com');
sleep(1);
}
Explanation
Starts with 10 users.
Spikes suddenly to 500 users in 10 seconds.
Drops back to 10 users.
Helps determine how the system handles sudden surges in traffic.
Analyzing Test Results
After running the tests, K6 provides detailed statistics
Stress testing is vital to ensure application stability and scalability. Using K6, we can simulate different stress scenarios like ramping load, constant high load, and spikes to identify system weaknesses before they affect users.
Load testing is essential to evaluate how a system behaves under expected and peak loads. Traditionally, we rely on metrics like requests per second (RPS), response time, and error rates. However, an insightful approach called Average Load Testing has been discussed recently. This blog explores that concept in detail, providing practical examples to help you apply it effectively.
Understanding Average Load Testing
Average Load Testing focuses on simulating real-world load patterns rather than traditional peak load tests. Instead of sending a fixed number of requests per second, this approach
Generates requests based on the average concurrency over time.
More accurately reflects real-world traffic patterns.
Helps identify performance bottlenecks in a realistic manner.
Setting Up Load Testing with K6
K6 is an excellent tool for implementing Average Load Testing. Letβs go through practical examples of setting up such tests.
The ramping-arrival-rate gradually increases requests per second over time.
The stages array defines a progression from 5 to 100 requests/sec over 6 minutes.
Logs response times to help analyze system performance.
Example 3: Load Testing with Multiple Endpoints
In real applications, multiple endpoints are often tested simultaneously. Hereβs how to test different API routes
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
scenarios: {
multiple_endpoints: {
executor: 'constant-arrival-rate',
rate: 15, // 15 requests per second
timeUnit: '1s',
duration: '2m',
preAllocatedVUs: 30,
maxVUs: 60,
},
},
};
export default function () {
let urls = [
'https://test-api.example.com/users',
'https://test-api.example.com/orders',
'https://test-api.example.com/products'
];
let res = http.get(urls[Math.floor(Math.random() * urls.length)]);
check(res, {
'is status 200': (r) => r.status === 200,
});
console.log(`Response time: ${res.timings.duration}ms`);
sleep(1);
}
Explanation
The script randomly selects an API endpoint to test different routes.
Uses check to ensure status codes are 200.
Logs response times for deeper insights.
Analyzing Results
To analyze test results, you can store logs or metrics in a database or monitoring tool and visualize trends over time. Some popular options include
Prometheus for time-series data storage.
InfluxDB for handling large-scale performance metrics.
ELK Stack (Elasticsearch, Logstash, Kibana) for log-based analysis.
Average Load Testing provides a more realistic way to measure system performance. By leveraging K6, you can create flexible, real-world simulations to optimize your applications effectively.
The Web storage api is a set of mechanisms that enable browsers to store key-value pairs. Before HTML5, application data had to be sorted in cookies, included in every server request. Its intended to be far more user-friendly than using cookies.
Web storage is more secure, and large amounts of data can be stored locally, without affecting website performance.
There are 2 types of web storage,
Local Storage
Session Storage
We already have cookies. Why additional objects?
Unlike cookies, web storage objects are not sent to server with each request. Because of that, we can store much more. Most modern browsers allow at least 5 megabytes of data (or more) and have settings to configure that.
Also unlike cookies, the server canβt manipulate storage objects via HTTP headers. Everythingβs done in JavaScript.The storage is bound to the origin (domain/protocol/port triplet). That is, different protocols or subdomains infer different storage objects, they canβt access data from each other.
In this guide, you will learn/refresh about LocalStorage.
LocalStorage
The localStorage is property of the window (browser window object) interface allows you to access a Storage object for the Documentβs origin; the stored data is saved across browser sessions.
Data is kept for a longtime in local storage (with no expiration date.). This could be one day, one week, or even one year as per the developer preference ( Data in local storage maintained even if the browser is closed).
Local storage only stores strings. So, if you intend to store objects, lists or arrays, you must convert them into a string using JSON.stringfy()
Local storage will be available via the window.localstorage property.
Whatβs interesting about them is that the data survives a page refresh (for sessionStorage) and even a full browser restart (for localStorage).
Functionalities
// setItem normal strings
window.localStorage.setItem("name", "goku");
// getItem
const name = window.localStorage.getItem("name");
console.log("name from localstorage, "+name);
// Storing an Object without JSON stringify
const data = {
"commodity":"apple",
"price":43
};
window.localStorage.setItem('commodity', data);
var result = window.localStorage.getItem('commodity');
console.log("Retrived data without jsonified, "+ result);
// Storing an object after converting to JSON string.
var jsonifiedString = JSON.stringify(data);
window.localStorage.setItem('commodity', jsonifiedString);
var result = window.localStorage.getItem('commodity');
console.log("Retrived data after jsonified, "+ result);
// remove item
window.localStorage.removeItem("commodity");
var result = window.localStorage.getItem('commodity');
console.log("Data after removing the key "+ result);
//length
console.log("length of local storage " + window.localStorage.length);
// clear
window.localStorage.clear();
console.log("length of local storage - after clear " + window.localStorage.length);
When to use Local Storage
Data stored in Local Storage can be easily accessed by third party individuals.
So its important to know that any sensitive data must not sorted in Local Storage.
Local Storage can help in storing temporary data before it is pushed to the server.
Always clear local storage once the operation is completed.
The majority of local storageβs drawbacks arenβt really significant. You may still not use it, but your app will run a little slower and youβll experience a tiny developer inconvenience. Security, however, is distinct. Knowing and understanding the security model of local storage is crucial since it will have a significant impact on your website in ways you might not have anticipated.
Local storage also has the drawback of being insecure. In no way! Everyone who stores sensitive information in local storage, such as session data, user information, credit card information (even momentarily! ), and anything else you wouldnβt want shared publicly on social media, is doing it incorrectly.
The purpose of local storage in a browser for safe storage was not intended. It was intended to be a straightforward key/value store for strings only that programmers could use to create somewhat more complicated single page apps.
General Preventions
For example, if we are using third party JavaScript libraries and they are injected with some scripts which extract the storage objects, our storage data wonβt be secure anymore. Therefore itβs not recommended to save sensitive data as
Username/Password
Credit card info
JWT tokens
API keys
Personal info
Session ids
Do not use the same origin for multiple web applications. Instead, use subdomains since otherwise, the storage will be shared with all. Reason is, for each subdomain it will have an unique localstorage; and they canβt communicate between subdomain instances.
Once some data are stored in Local storage, the developers donβt have any control over it until the user clears it. If you want the data to be removed once the session ends, use SessionStorage.
Validate, encode and escape data read from browser storage
What do Reddit, Discord, Medium, and LinkedIn have in common? They use whatβs called a skeleton loading screen for their applications. A skeleton screen is essentially a wireframe of the application. The wireframe is a placeholder until the application finally loads.
Rise of skeleton loader.
The term βskeleton screenβ was introduced in 2013 by product designer Luke Wroblewski in a blog post about reducing perceived wait time. In this lukew.com/ff/entry.asp?1797 post, he explains how gradually revealing page content turns user attention to the content being loaded, and off of the loading time itself.
Skeleton Loader
Skeleton loading screens will improve your applicationβs user experience andΒ make it feel more performant. The skeleton loading screen essentiallyΒ impersonates the original layout.
This lets the user know whatβs happening on the screen. The user interprets this as the application is booting up and the content is loading.
In simplest terms, Skeleton Loader is a static / animated placeholder for the information that is still loading. It mimic the structure and look of the entire view.
Why not just a loading spinner ?
Instead of showing a loading spinner, we could show a skeleton screen that makes the user see that there is progress happening when launching and navigating the application.
They let the user know that some content is loading and, more importantly, provide an indication of what is loading, whether itβs an image, text, card, and so on.
This gives the user the impression that the website is faster because they already know what type of content is loading before it appears. This is referred to asΒ perceived performance.
Skeleton screensΒ donβt really make pages load faster. Instead, they are designed to make it feel like pages are loading faster.
When to use ?
Use on high-traffic pages where resources takes a bit long to load like account dashboard.
When the component containsΒ good amount of information, such as list or card.
Could be replaced byΒ spinΒ in any situation, but can provide a better user experience.
Use when thereβs more than 1 element loading at the same time that requires an indicator.
Use when you need to load multiple images at once, a skeleton screen might make a good placeholder. For these pages, consider implementing lazy loading first, which is a similar technique for decreasing perceived load time.
When not to use ?
Not to use for a long-running process, e.g. importing data, manipulation of data etc. (Operations on data intensive applications)
Not to use for fast processes that that takeΒ less than half a second.
Users still associate video buffering with spinners. Avoid skeleton screens any time a video is loading on your page.
For longer processes (uploads, download, file manipulation ) can use progress bar instead of skeleton loading.
As a replacement for poor performance: If you can further optimize your website to actually load content more quickly, always pursue that first.
Caching is an essential technique for improving application performance and reducing the load on databases. However, improper caching strategies can lead to serious issues.
In this blog, we will discuss four common cache problems: Thundering Herd Problem, Cache Penetration, Cache Breakdown, and Cache Crash, along with their causes, consequences, and solutions.
The Thundering Herd Problem occurs when a large number of keys in the cache expire at the same time. When this happens, all requests bypass the cache and hit the database simultaneously, overwhelming it and causing performance degradation or even a system crash.
Example Scenario
Imagine an e-commerce website where product details are cached for 10 minutes. If all the productsβ cache expires at the same time, thousands of users sending requests will cause an overwhelming load on the database.
Solutions
Staggered Expiration: Instead of setting a fixed expiration time for all keys, introduce a random expiry variation.
Allow Only Core Business Queries: Limit direct database access only to core business data, while returning stale data or temporary placeholders for less critical data.
Lazy Rebuild Strategy: Instead of all requests querying the database, the first request fetches data and updates the cache while others wait.
Batch Processing: Queue multiple requests and process them in batches to reduce database load.
Cache Penetration
What is it?
Cache Penetration occurs when requests are made for keys that neither exist in the cache nor in the database. Since these requests always hit the database, they put excessive pressure on the system.
Example Scenario
A malicious user could attempt to query random user IDs that do not exist, forcing the system to repeatedly query the database and skip the cache.
Solutions
Cache Null Values: If a key does not exist in the database, store a null value in the cache to prevent unnecessary database queries.
Use a Bloom Filter: A Bloom filter helps check whether a key exists before querying the database. If the Bloom filter does not contain the key, the request is discarded immediately.
Rate Limiting: Implement request throttling to prevent excessive access to non-existent keys.
Data Prefetching: Predict and load commonly accessed data into the cache before it is needed.
Cache Breakdown
What is it?
Cache Breakdown is similar to the Thundering Herd Problem, but it occurs specifically when a single hot key (a frequently accessed key) expires. This results in a surge of database queries as all users try to retrieve the same data.
Example Scenario
A social media platform caches trending hashtags. If the cache expires, millions of users will query the same hashtag at once, hitting the database hard.
Solutions
Never Expire Hot Keys: Keep hot keys permanently in the cache unless an update is required.
Preload the Cache: Refresh the cache asynchronously before expiration by setting a background task to update the cache regularly.
Mutex Locking: Ensure only one request updates the cache, while others wait for the update to complete.
Double Buffering: Maintain a secondary cache layer to serve requests while the primary cache is being refreshed.
Cache Crash
What is it?
A Cache Crash occurs when the cache service itself goes down. When this happens, all requests fall back to the database, overloading it and causing severe performance issues.
Example Scenario
If a Redis instance storing session data for a web application crashes, all authentication requests will be forced to hit the database, leading to a potential outage.
Solutions
Cache Clustering: Use a cluster of cache nodes instead of a single instance to ensure high availability.
Persistent Storage for Cache: Enable persistence modes like Redis RDB or AOF to recover data quickly after a crash.
Automatic Failover: Configure automated failover with tools like Redis Sentinel to ensure availability even if a node fails.
Circuit Breaker Mechanism: Prevent the application from directly accessing the database if the cache is unavailable, reducing the impact of a crash.
Caching is a powerful mechanism to improve application performance, but improper strategies can lead to severe bottlenecks. Problems like Thundering Herd, Cache Penetration, Cache Breakdown, and Cache Crash can significantly degrade system reliability if not handled properly.
Today, I learnt about various locking mechanism to prevent double update. In this blog, i make notes on Shared Lock and Exclusive Lock for my future self.
What Are Locks in Databases?
Locks are mechanisms used by a DBMS to control access to data. They ensure that transactions are executed in a way that maintains the ACID (Atomicity, Consistency, Isolation, Durability) properties of the database. Locks can be classified into several types, including
Shared Locks (S Locks): Allow multiple transactions to read a resource simultaneously but prevent any transaction from writing to it.
Exclusive Locks (X Locks): Allow a single transaction to modify a resource, preventing both reading and writing by other transactions.
Intent Locks: Used to signal the type of lock a transaction intends to acquire at a lower level.
Deadlock Prevention Locks: Special locks aimed at preventing deadlock scenarios.
Shared Lock
A shared lock is used when a transaction needs to read a resource (e.g., a database row or table) without altering it. Multiple transactions can acquire a shared lock on the same resource simultaneously. However, as long as one or more shared locks exist on a resource, no transaction can acquire an exclusive lock on that resource.
-- Transaction A: Acquire a shared lock on a row
BEGIN;
SELECT * FROM employees WHERE id = 1 FOR SHARE;
-- Transaction B: Acquire a shared lock on the same row
BEGIN;
SELECT * FROM employees WHERE id = 1 FOR SHARE;
-- Both transactions can read the row concurrently
-- Transaction C: Attempt to update the same row
BEGIN;
UPDATE employees SET salary = salary + 1000 WHERE id = 1;
-- Transaction C will be blocked until Transactions A and B release their locks
Key Characteristics of Shared Locks
1. Concurrent Reads
Shared locks allow multiple transactions to read the same resource at the same time.
This is ideal for operations like SELECT queries that do not modify data.
2. Write Blocking
While a shared lock is active, no transaction can modify the locked resource.
Prevents dirty writes and ensures read consistency.
3. Compatibility
Shared locks are compatible with other shared locks but not with exclusive locks.
When Are Shared Locks Used?
Shared locks are typically employed in read operations under certain isolation levels. For instance,
1. Read Committed Isolation Level:
Shared locks are held for the duration of the read operation.
Prevents dirty reads by ensuring the data being read is not modified by other transactions during the read.
2. Repeatable Read Isolation Level:
Shared locks are held until the transaction completes.
Ensures that the data read during a transaction remains consistent and unmodified.
3. Snapshot Isolation:
Shared locks may not be explicitly used, as the DBMS creates a consistent snapshot of the data for the transaction.
Exclusive Locks
An exclusive lock is used when a transaction needs to modify a resource. Only one transaction can hold an exclusive lock on a resource at a time, ensuring no other transactions can read or write to the locked resource.
-- Transaction X: Acquire an exclusive lock to update a row
BEGIN;
UPDATE employees SET salary = salary + 1000 WHERE id = 2;
-- Transaction Y: Attempt to read the same row
BEGIN;
SELECT * FROM employees WHERE id = 2;
-- Transaction Y will be blocked until Transaction X completes
-- Transaction Z: Attempt to update the same row
BEGIN;
UPDATE employees SET salary = salary + 500 WHERE id = 2;
-- Transaction Z will also be blocked until Transaction X completes
Key Characteristics of Exclusive Locks
1. Write Operations: Exclusive locks are essential for operations like INSERT, UPDATE, and DELETE.
2. Blocking Reads and Writes: While an exclusive lock is active, no other transaction can read or write to the resource.
3. Isolation: Ensures that changes made by one transaction are not visible to others until the transaction is complete.
When Are Exclusive Locks Used?
Exclusive locks are typically employed in write operations or any operation that modifies the database. For instance:
1. Transactional Updates β A transaction that updates a row acquires an exclusive lock to ensure no other transaction can access or modify the row during the update.
2. Table Modifications β When altering a table structure, the DBMS may place an exclusive lock on the entire table.
Benefits of Shared and Exclusive Locks
Benefits of Shared Locks
Consistency in Multi-User Environments β Ensure that data being read is not altered by other transactions, preserving consistency.
Concurrency Support β Allow multiple transactions to read data simultaneously, improving system performance.
Data Integrity β Prevent dirty reads and writes, ensuring that operations yield reliable results.
Benefits of Exclusive Locks
Data Integrity During Modifications β Prevents other transactions from accessing data being modified, ensuring changes are applied safely.
Isolation of Transactions β Ensures that modifications by one transaction are not visible to others until committed.
Limitations and Challenges
Shared Locks
Potential for Deadlocks β Deadlocks can occur if two transactions simultaneously hold shared locks and attempt to upgrade to exclusive locks.
Blocking Writes β Shared locks can delay write operations, potentially impacting performance in write-heavy systems.
Lock Escalation β In systems with high concurrency, shared locks may escalate to table-level locks, reducing granularity and concurrency.
Exclusive Locks
Reduced Concurrency β Exclusive locks prevent other transactions from accessing the locked resource, which can lead to bottlenecks in highly concurrent systems.
Risk of Deadlocks β Deadlocks can occur if two transactions attempt to acquire exclusive locks on resources held by each other.
Early Morning today, i watched a video on partitioning and sharding. In that video, Arpit explained the limitation of Vertical Scaling and ways to infinite scale DB with Sharding and Partitioning. In this blog, i jot down notes on partioining with single node implementation with postgres for my future self.
As the volume of data grows, managing databases efficiently becomes critical and when we understood that vertical scaling has its limits, we have two common strategies to handle large datasets are partitioning and sharding. While they may sound similar, these techniques serve different purposes and are implemented differently. Letβs explore these concepts in detail.
What is Partitioning?
Partitioning involves dividing a large dataset into smaller, manageable segments, known as partitions. Each partition is stored separately but remains part of a single database instance. Partitioning is typically used to improve query performance and manageability.
Types of Partitioning
1. Range Partitioning
Data is divided based on ranges of a columnβs values.
Example: A table storing customer orders might partition data by order date: January orders in one partition, February orders in another.
PostgreSQL Example
CREATE TABLE orders (
id SERIAL,
customer_id INT,
order_date DATE NOT NULL,
PRIMARY KEY (id, order_date) -- Include the partition key
) PARTITION BY RANGE (order_date);
CREATE TABLE orders_jan PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE orders_feb PARTITION OF orders
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
2. Hash Partitioning
A hash function determines the partition where a record will be stored.
Example: Orders can be distributed across partitions based on the hash of the customer ID.
Postgres Example
CREATE TABLE orders (
id SERIAL ,
customer_id INT,
order_date DATE NOT NULL,
PRIMARY KEY (id, customer_id)
) PARTITION BY HASH (customer_id, id);
CREATE TABLE orders_part_1 PARTITION OF orders
FOR VALUES WITH (MODULUS 2, REMAINDER 0);
CREATE TABLE orders_part_2 PARTITION OF orders
FOR VALUES WITH (MODULUS 2, REMAINDER 1);
3. List Partitioning
Data is divided based on a predefined list of values.
Example: A table storing sales data could partition based on regions: North, South, East, and West
Postgres Example
CREATE TABLE sales (
id SERIAL ,
region TEXT NOT NULL,
amount NUMERIC,
PRIMARY KEY (id, region)
) PARTITION BY LIST (region);
CREATE TABLE sales_north PARTITION OF sales
FOR VALUES IN ('North');
CREATE TABLE sales_south PARTITION OF sales
FOR VALUES IN ('South');
4. Composite Partitioning
Combines two or more partitioning strategies, such as range and list partitioning.
Example: A table partitioned by range on order date and sub-partitioned by list on region.
Postgres Example
CREATE TABLE orders (
id SERIAL,
customer_id INT,
order_date DATE NOT NULL,
region TEXT NOT NULL,
PRIMARY KEY (id, order_date, region)
) PARTITION BY RANGE (order_date);
CREATE TABLE orders_2024 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01')
PARTITION BY LIST (region);
CREATE TABLE orders_2024_north PARTITION OF orders_2024
FOR VALUES IN ('North');
CREATE TABLE orders_2024_south PARTITION OF orders_2024
FOR VALUES IN ('South');
Locust provides powerful event hooks, such as test_start and test_stop, to execute custom logic before and after a load test begins or ends. These events allow you to implement setup and teardown operations at the test level, which applies to the entire test run rather than individual users.
In this blog, we will
Understand what test_start and test_stop are.
Explore their use cases.
Provide examples of implementing these events.
Discuss how to run and validate the setup.
What Are test_start and test_stop?
test_start: Triggered when the test starts. Use this event to perform actions like initializing global resources, starting external systems, or logging test start information.
test_stop: Triggered when the test ends. This event is ideal for cleanup operations, aggregating results, or stopping external systems.
These events are global and apply to the entire test environment rather than individual user instances.
Why Use test_start and test_stop?
Global Setup: Initialize shared resources, like database connections or external services.
Logging: Record timestamps or test details for audit or reporting purposes.
External System Management: Start/stop services that the test depends on, such as mock servers or third-party APIs.
Example: Basic Usage of test_start and test_stop
Hereβs a basic example demonstrating the usage of these events
from locust import User, task, between, events
from datetime import datetime
# Global setup: Perform actions at test start
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
print("Test started at:", datetime.now())
# Global teardown: Perform actions at test stop
@events.test_stop.add_listener
def on_test_stop(environment, **kwargs):
print("Test stopped at:", datetime.now())
# Simulated user behavior
class MyUser(User):
wait_time = between(1, 5)
@task
def print_datetime(self):
"""Task that prints the current datetime."""
print("Current datetime:", datetime.now())
Running the Example
Save the code as locustfile.py.
Start Locust -> `locust -f locustfile.py`
Configure the test parameters (number of users, spawn rate, etc.) in the web UI at http://localhost:8089.
Observe the console output:
A message when the test starts (on_test_start).
Messages during the test as users execute tasks.
A message when the test stops (on_test_stop).
Example: Logging Test Details
You can log detailed test information, like the number of users and host under test, using environment and kwargs
Locust provides two special methods, on_start and on_stop, to handle setup and teardown actions for individual users. These methods allow you to execute specific code when a simulated user starts or stops, making it easier to simulate real-world scenarios like login/logout or initialization tasks.
In this blog, weβll cover,
What on_start and on_stop do.
Why they are important.
Practical examples of using these methods.
Running and testing Locust scripts.
What Are on_start and on_stop?
on_start: This method is executed once when a new simulated user starts. Itβs commonly used for tasks like logging in or setting up the environment.
on_stop: This method is executed once when a simulated user stops. Itβs often used for cleanup tasks like logging out.
These methods are executed only once per user during the lifecycle of a test, as opposed to tasks that are run repeatedly.
Why Use on_start and on_stop?
Simulating Real User Behavior: Real users often start a session with an action (e.g., login) and end it with another (e.g., logout).
Initial Setup: Some tasks require initializing data or setting up user state before performing other actions.
Cleanup: Ensure that actions like logout are performed to leave the system in a clean state.
Examples
Basic Usage of on_start and on_stop
In this example, we just print on start and `on stop` for each user while running a task.
from locust import User, task, between, constant, constant_pacing
from datetime import datetime
class MyUser(User):
wait_time = between(1, 5)
def on_start(self):
print("on start")
def on_stop(self):
print("on stop")
@task
def print_datetime(self):
print(datetime.now())
Locust allows you to define multiple user types in your load tests, enabling you to simulate different user behaviors and traffic patterns. This is particularly useful when your application serves diverse client types, such as web and mobile users, each with unique interaction patterns.
In this blog, we will
Discuss the concept of multiple user types in Locust.
Explore how to implement multiple user classes with weights.
Run and analyze the test results.
Why Use Multiple User Types?
In real-world applications, different user groups interact with your system differently. For example,
Web Users might spend more time browsing through the UI.
Mobile Users could make faster but more frequent requests.
By simulating distinct user types with varying behaviors, you can identify performance bottlenecks across all client groups.
Understanding User Classes and Weights
Locust provides the ability to define user classes by extending the User or HttpUser base class. Each user class can,
Have a unique set of tasks.
Define its own wait times.
Be assigned a weight, which determines the proportion of that user type in the simulation.
For example, if WebUser has a weight of 1 and MobileUser has a weight of 2, the simulation will spawn 1 web user for every 2 mobile users.
Example: Simulating Web and Mobile Users
Below is an example Locust test with two user types
from locust import User, task, between
# Define a user class for web users
class MyWebUser(User):
wait_time = between(1, 3) # Web users wait between 1 and 3 seconds between tasks
weight = 1 # Web users are less frequent
@task
def login_url(self):
print("I am logging in as a Web User")
# Define a user class for mobile users
class MyMobileUser(User):
wait_time = between(1, 3) # Mobile users wait between 1 and 3 seconds
weight = 2 # Mobile users are more frequent
@task
def login_url(self):
print("I am logging in as a Mobile User")
How Locust Uses Weights
With the above configuration
For every 3 users spawned, 1 will be a Web User, and 2 will be Mobile Users (based on their weights: 1 and 2).
Locust automatically handles spawning these users in the specified ratio.
Running the Locust Test
Save the Code Save the above code in a file named locustfile.py.
Start Locust Open your terminal and run `locust -f locustfile.py`
Host: If you are testing an actual API or website, specify its URL (e.g., http://localhost:8000).
Analyze Results
Observe how Locust spawns the users according to their weights and tracks metrics like request counts and response times.
After running the test:
Check the distribution of requests to ensure it matches the weight ratio (e.g., for every 1 web user request, there should be ~3 mobile user requests).
Use the metrics (response time, failure rate) to evaluate performance for each user type.
Locust is an excellent load testing tool, enabling developers to simulate concurrent user traffic on their applications. One of its powerful features is wait times, which simulate the realistic user think time between consecutive tasks. By customizing wait times, you can emulate user behavior more effectively, making your tests reflect actual usage patterns.
In this blog, weβll cover,
What wait times are in Locust.
Built-in wait time options.
Creating custom wait times.
A full example with instructions to run the test.
What Are Wait Times in Locust?
In real-world scenarios, users donβt interact with applications continuously. After performing an action (e.g., submitting a form), they often pause before the next action. This pause is called a wait time in Locust, and it plays a crucial role in mimicking real-life user behavior.
Locust provides several ways to define these wait times within your test scenarios.
FastAPI App Overview
Hereβs the FastAPI app that weβll test,
from fastapi import FastAPI
# Create a FastAPI app instance
app = FastAPI()
# Define a route with a GET method
@app.get("/")
def read_root():
return {"message": "Welcome to FastAPI!"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
return {"item_id": item_id, "q": q}
Locust Examples for FastAPI
1. Constant Wait Time Example
Here, weβll simulate constant pauses between user requests
from locust import HttpUser, task, constant
class FastAPIUser(HttpUser):
wait_time = constant(2) # Wait for 2 seconds between requests
@task
def get_root(self):
self.client.get("/") # Simulates a GET request to the root endpoint
@task
def get_item(self):
self.client.get("/items/42?q=test") # Simulates a GET request with path and query parameters
2. Between wait time Example
Simulating random pauses between requests.
from locust import HttpUser, task, between
class FastAPIUser(HttpUser):
wait_time = between(1, 5) # Random wait time between 1 and 5 seconds
@task(3) # Weighted task: this runs 3 times more often
def get_root(self):
self.client.get("/")
@task(1)
def get_item(self):
self.client.get("/items/10?q=locust")
3. Custom Wait Time Example
Using a custom wait time function to introduce more complex user behavior
import random
from locust import HttpUser, task
def custom_wait():
return max(1, random.normalvariate(3, 1)) # Normal distribution (mean: 3s, stddev: 1s)
class FastAPIUser(HttpUser):
wait_time = custom_wait
@task
def get_root(self):
self.client.get("/")
@task
def get_item(self):
self.client.get("/items/99?q=custom")
Full Test Example
Combining all the above elements, hereβs a complete Locust test for your FastAPI app.
from locust import HttpUser, task, between
import random
# Custom wait time function
def custom_wait():
return max(1, random.uniform(1, 3)) # Random wait time between 1 and 3 seconds
class FastAPIUser(HttpUser):
wait_time = custom_wait # Use the custom wait time
@task(3)
def browse_homepage(self):
"""Simulates browsing the root endpoint."""
self.client.get("/")
@task(1)
def browse_item(self):
"""Simulates fetching an item with ID and query parameter."""
item_id = random.randint(1, 100)
self.client.get(f"/items/{item_id}?q=test")
Running Locust for FastAPI
Run Your FastAPI App Save the FastAPI app code in a file (e.g., main.py) and start the server
uvicorn main:app --reload
By default, the app will run on http://127.0.0.1:8000.
2. Run Locust Save the Locust file as locustfile.py and start Locust.
In todayβs fast-paced digital application, delivering a reliable and scalable application is key to providing a positive user experience.
One of the most effective ways to guarantee this is through load testing. This post will walk you through the fundamentals of load testing, real-time examples of its application, and crucial metrics to watch for.
What is Load Testing?
Load testing is a type of performance testing that simulates real-world usage of an application. By applying load to a system, testers observe how it behaves under peak and normal conditions. The primary goal is to identify any performance bottlenecks, ensure the system can handle expected user traffic, and maintain optimal performance.
Load testing answers these critical questions:
Can the application handle the expected user load?
How does performance degrade as the load increases?
What is the systemβs breaking point?
Why is Load Testing Important?
Without load testing, applications are vulnerable to crashes, slow response times, and unavailability, all of which can lead to a poor user experience, lost revenue, and brand damage. Proactive load testing allows teams to address issues before they impact end-users.
Real-Time Load Testing Examples
Letβs explore some real-world examples that demonstrate the importance of load testing.
Example 1: E-commerce Website During a Sale Event
An online retailer preparing for a Black Friday sale knows that traffic will spike. They conduct load testing to simulate thousands of users browsing, adding items to their cart, and checking out simultaneously. By analyzing the systemβs response under these conditions, the retailer can identify weak points in the checkout process or database and make necessary optimizations.
Example 2: Video Streaming Platform Launch
A new streaming platform is preparing for launch, expecting millions of users. Through load testing, the team simulates high traffic, testing how well video streaming performs under maximum user load. This testing also helps check if CDN (Content Delivery Network) configurations are optimized for global access, ensuring minimal buffering and downtime during peak hours.
Example 3: Financial Services Platform During Market Hours
A trading platform experiences intense usage during market open and close hours. Load testing helps simulate these peak times, ensuring that real-time data updates, transactions, and account management work flawlessly. Testing for these scenarios helps avoid issues like slow trade executions and platform unavailability during critical trading periods.
Key Metrics to Monitor in Load Testing
Understanding key metrics is essential for interpreting load test results. Here are some critical metrics to focus on:
1. Response Time
Definition: The time taken by the system to respond to a request.
Why It Matters: Slow response times can frustrate users and indicate bottlenecks.
Example Thresholds: For websites, a response time below 2 seconds is considered acceptable.
2. Throughput
Definition: The number of requests processed per second.
Why It Matters: Throughput indicates how many concurrent users your application can handle.
Real-Time Use Case: In our e-commerce example, the retailer would track throughput to ensure the checkout process doesnβt become a bottleneck.
3. Error Rate
Definition: The percentage of failed requests out of total requests.
Why It Matters: A high error rate could indicate application instability under load.
Real-Time Use Case: The trading platform monitors the error rate during market close, ensuring the system doesnβt throw errors under peak trading load.
4. CPU and Memory Utilization
Definition: The percentage of CPU and memory resources used during the load test.
Why It Matters: High CPU or memory utilization can signal that the server may not handle additional load.
Real-Time Use Case: The video streaming platform tracks memory usage to prevent lag or interruptions in streaming as users increase.
5. Concurrent Users
Definition: The number of users active on the application at the same time.
Why It Matters: Concurrent users help you understand how much load the system can handle before performance starts degrading.
Real-Time Use Case: The retailer tests how many concurrent users can shop simultaneously without crashing the website.
6. Latency
Definition: The time it takes for a request to travel from the client to the server and back.
Why It Matters: High latency indicates network or processing delays that can slow down the user experience.
Real-Time Use Case: For a financial app, reducing latency ensures trades execute in near real-time, which is crucial for users during volatile market conditions.
7. 95th and 99th Percentile Response Times
Definition: The time within which 95% or 99% of requests are completed.
Why It Matters: These percentiles help identify outliers that may impact user experience.
Real-Time Use Case: The streaming service may analyze these percentiles to ensure smooth playback for most users, even under peak loads.
Best Practices for Effective Load Testing
Set Clear Objectives: Define specific goals, such as the expected number of concurrent users or acceptable response times, based on the nature of the application.
Use Realistic Load Scenarios: Create scenarios that mimic actual user behavior, including peak times, user interactions, and geographical diversity.
Analyze Bottlenecks and Optimize: Use test results to identify and address performance bottlenecks, whether in the application code, database queries, or server configurations.
Monitor in Real-Time: Track metrics like response time, throughput, and error rates in real-time to identify issues as they arise during the test.
Repeat and Compare: Conduct multiple load tests to ensure consistent performance over time, especially after any significant update or release.
Load testing is crucial for building a resilient and scalable application. By using real-world scenarios and keeping a close eye on metrics like response time, throughput, and error rates, you can ensure your system performs well under load. Proactive load testing helps to deliver a smooth, reliable experience for users, even during peak times.