
Introduction
Databases sit behind nearly every modern application, and they often become the first serious bottleneck as traffic grows. Whether you run PostgreSQL, MySQL, SQL Server, MongoDB, Cassandra, or another datastore, database performance directly affects page load times, API latency, transaction throughput, and overall user experience. That’s why load testing databases is a critical part of performance testing and stress testing before production.
While teams rarely expose a database directly to end users, they do expose database-backed APIs, internal services, admin tools, and query gateways. In practice, the most realistic way to load test databases is often through the application or service layer that executes real queries, transactions, and writes against the database. This gives you accurate visibility into how your database behaves under realistic concurrency, authentication, connection pooling, caching, indexing, and transaction patterns.
In this guide, you’ll learn how to load test databases with LoadForge using Locust-based Python scripts. We’ll cover both SQL and NoSQL-style workloads, show realistic authentication flows, and walk through scenarios like read-heavy traffic, transactional writes, analytics queries, and bulk ingestion. You’ll also see how LoadForge helps with distributed testing, real-time reporting, cloud-based infrastructure, global test locations, and CI/CD integration so you can catch database bottlenecks before they impact production.
Prerequisites
Before you begin load testing databases with LoadForge, make sure you have:
- A LoadForge account
- A target environment such as staging, pre-production, or a dedicated performance environment
- An application, API, or database gateway that interacts with your database
- Test credentials for authenticated endpoints
- Representative test data in your database
- An understanding of your key database-backed user flows
You should also identify:
- Read-heavy endpoints
- Write-heavy endpoints
- Transactional workflows
- Reporting or aggregation queries
- Batch or bulk import operations
- Expected concurrency and throughput targets
For realistic database load testing, avoid testing against an empty or tiny dataset. Query planners, indexes, cache hit rates, and disk behavior can look very different when the database contains production-like data volumes.
If possible, gather baseline observability beforehand:
- Database CPU and memory usage
- Disk IOPS and storage latency
- Query execution times
- Slow query logs
- Connection pool saturation
- Lock contention and deadlocks
- Replication lag for distributed databases
- Cache hit ratios
Understanding Databases Under Load
Databases behave differently under concurrent load than stateless web servers. A web tier can often scale horizontally quickly, but databases are constrained by storage, locks, indexes, query complexity, replication, and transaction coordination.
Common SQL database bottlenecks
For relational databases like PostgreSQL and MySQL, the most common performance issues under load include:
- Slow queries due to missing or inefficient indexes
- Table scans on large datasets
- Lock contention during updates or transactions
- Connection pool exhaustion
- High write amplification from indexes
- Expensive joins and aggregations
- Disk I/O saturation
- Replication lag under write-heavy workloads
Common NoSQL database bottlenecks
For NoSQL systems like MongoDB, Cassandra, DynamoDB-compatible services, or Elasticsearch-backed data APIs, common issues include:
- Hot partitions or uneven key distribution
- Large document reads or writes
- Inefficient filters that bypass indexes
- High write latency during bursts
- Consistency and replication overhead
- Secondary index pressure
- Large aggregation pipelines
- Bulk ingestion overwhelming the cluster
Why API-level load testing is often best
Direct database benchmarking tools are useful, but they don’t always reflect real application behavior. Your app may add:
- Authentication and authorization
- Connection pooling
- ORM-generated queries
- Validation logic
- Cache layers
- Retry behavior
- Background jobs
- Transaction wrapping
Load testing through realistic endpoints lets you evaluate the full system path while still surfacing database bottlenecks. With LoadForge, you can run distributed load tests from multiple regions and observe response times, error rates, throughput, and concurrency in real time.
Writing Your First Load Test
Let’s start with a basic read-heavy SQL scenario. Imagine you have an e-commerce API backed by PostgreSQL. The application exposes product catalog and order lookup endpoints, both of which hit the database.
This first test simulates users browsing products and checking order history.
from locust import HttpUser, task, between
import random
class DatabaseBackedApiUser(HttpUser):
wait_time = between(1, 3)
def on_start(self):
self.customer_id = random.randint(1000, 5000)
@task(5)
def list_products(self):
category = random.choice(["laptops", "monitors", "keyboards", "storage"])
self.client.get(
f"/api/v1/products?category={category}&limit=25&sort=popularity",
name="/api/v1/products"
)
@task(3)
def product_search(self):
query = random.choice(["gaming laptop", "4k monitor", "mechanical keyboard", "ssd 1tb"])
self.client.get(
f"/api/v1/search?q={query}&limit=20",
name="/api/v1/search"
)
@task(2)
def order_history(self):
self.client.get(
f"/api/v1/customers/{self.customer_id}/orders?limit=10",
name="/api/v1/customers/[id]/orders"
)What this test does
This script creates a realistic read-heavy workload:
- Product listing likely triggers indexed filtering and sorting
- Search may hit full-text indexes or fallback SQL queries
- Order history tests customer-specific lookups and joins
The task weighting matters:
list_productsruns most oftenproduct_searchis moderately frequentorder_historyis less common but still important
This mirrors real traffic patterns better than assigning equal weight to every endpoint.
Why this is useful for database performance testing
Even a simple test like this can reveal:
- Slow product filtering queries
- Unindexed search conditions
- Poor join performance on orders
- Connection pool bottlenecks
- Cache effectiveness under concurrency
When you run this in LoadForge, start with a modest user count, then scale up gradually. Use LoadForge’s real-time reporting to watch response time percentiles, throughput, and failure rates as load increases.
Advanced Load Testing Scenarios
Once you have a baseline, move to more realistic and stressful database scenarios. The following examples cover authenticated access, transactional write workloads, NoSQL document queries, and bulk ingestion.
Scenario 1: Authenticated transactional workload for SQL databases
In many systems, the most important database activity happens after login. Let’s simulate an authenticated user placing orders in a PostgreSQL-backed API.
from locust import HttpUser, task, between
import random
import uuid
class EcommerceTransactionalUser(HttpUser):
wait_time = between(1, 2)
def on_start(self):
credentials = {
"email": f"loadtest_user_{random.randint(1, 500)}@example.com",
"password": "TestPassword123!"
}
response = self.client.post(
"/api/v1/auth/login",
json=credentials,
name="/api/v1/auth/login"
)
if response.status_code == 200:
token = response.json().get("access_token")
self.client.headers.update({
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
})
@task(4)
def browse_inventory(self):
warehouse_id = random.choice([1, 2, 3, 4])
self.client.get(
f"/api/v1/inventory?warehouse_id={warehouse_id}&in_stock=true&limit=50",
name="/api/v1/inventory"
)
@task(2)
def add_to_cart(self):
payload = {
"product_id": random.randint(10000, 10200),
"quantity": random.randint(1, 3)
}
self.client.post(
"/api/v1/cart/items",
json=payload,
name="/api/v1/cart/items"
)
@task(1)
def checkout(self):
payload = {
"payment_method_id": "pm_test_visa_4242",
"shipping_address_id": random.randint(2000, 2500),
"idempotency_key": str(uuid.uuid4())
}
self.client.post(
"/api/v1/orders/checkout",
json=payload,
name="/api/v1/orders/checkout"
)Why this scenario matters
This test is valuable because it exercises several database-intensive operations:
- Login may query users and session tables
- Inventory reads may involve stock tables and warehouse filtering
- Cart updates often require transactional consistency
- Checkout can create orders, order items, payment records, and inventory deductions
This is where SQL databases often show lock contention, transaction latency, and write bottlenecks.
What to watch for
During this test, look for:
- Increased latency on checkout as concurrency rises
- Spikes in 409, 422, or 500 responses
- Signs of deadlocks or transaction retries
- Connection pool saturation in the app tier
- Slow writes due to index or foreign key overhead
Scenario 2: NoSQL document database workload with filtering and aggregation
Now let’s simulate an analytics-style API backed by MongoDB or another document database. This example reflects a SaaS reporting service where users query events and dashboards.
from locust import HttpUser, task, between
import random
from datetime import datetime, timedelta
class AnalyticsApiUser(HttpUser):
wait_time = between(2, 5)
def on_start(self):
response = self.client.post(
"/api/v1/session/token",
json={
"api_key": "lf_demo_analytics_key",
"workspace_id": "ws_2048"
},
name="/api/v1/session/token"
)
if response.status_code == 200:
token = response.json().get("token")
self.client.headers.update({
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
})
@task(5)
def query_events(self):
start_date = (datetime.utcnow() - timedelta(days=7)).strftime("%Y-%m-%dT00:00:00Z")
end_date = datetime.utcnow().strftime("%Y-%m-%dT23:59:59Z")
payload = {
"event_type": random.choice(["page_view", "signup", "purchase", "refund"]),
"filters": {
"country": random.choice(["US", "UK", "DE", "CA"]),
"device": random.choice(["mobile", "desktop", "tablet"])
},
"date_range": {
"from": start_date,
"to": end_date
},
"limit": 100
}
self.client.post(
"/api/v1/events/query",
json=payload,
name="/api/v1/events/query"
)
@task(2)
def aggregate_dashboard(self):
payload = {
"metrics": ["active_users", "conversion_rate", "revenue"],
"group_by": ["country", "device"],
"time_bucket": "day",
"date_range": {
"from": "2026-03-01T00:00:00Z",
"to": "2026-03-07T23:59:59Z"
}
}
self.client.post(
"/api/v1/dashboards/traffic/aggregate",
json=payload,
name="/api/v1/dashboards/[name]/aggregate"
)Why this scenario matters
Document databases and analytics stores often perform well for simple key lookups but slow down with:
- Broad filters on large collections
- Aggregation pipelines
- Group-by style reporting
- Time-range scans
- Poorly selective indexes
This script simulates realistic reporting traffic that can expose hot shards, expensive aggregations, or oversized documents.
Scenario 3: Bulk ingestion and write-heavy stress testing
Many database problems only appear during write bursts. For example, an event ingestion API may write batches into a NoSQL store, append-only log database, or partitioned SQL table.
from locust import HttpUser, task, between
import random
import uuid
from datetime import datetime
class EventIngestionUser(HttpUser):
wait_time = between(0.5, 1.5)
def on_start(self):
self.client.headers.update({
"Authorization": "Bearer ingest_service_token_abc123",
"Content-Type": "application/json"
})
@task
def ingest_batch(self):
events = []
for _ in range(50):
events.append({
"event_id": str(uuid.uuid4()),
"tenant_id": random.choice(["tenant_a", "tenant_b", "tenant_c", "tenant_d"]),
"user_id": random.randint(100000, 999999),
"event_type": random.choice(["click", "view", "purchase", "login", "logout"]),
"timestamp": datetime.utcnow().isoformat() + "Z",
"properties": {
"page": random.choice(["/home", "/pricing", "/checkout", "/docs"]),
"browser": random.choice(["Chrome", "Firefox", "Safari", "Edge"]),
"campaign_id": random.choice(["spring_launch", "retargeting_q2", "organic", "partner"])
}
})
self.client.post(
"/api/v1/ingestion/events:batchInsert",
json={"events": events},
name="/api/v1/ingestion/events:batchInsert"
)Why this scenario matters
Bulk ingestion can surface issues that read tests miss:
- Write throughput ceilings
- WAL or transaction log pressure
- Replication lag
- Partition hotspotting
- Queue buildup in ingestion services
- Storage write latency
- Secondary index overhead
This is a great scenario for stress testing because it pushes the database toward saturation quickly.
Analyzing Your Results
After running your database load test in LoadForge, focus on both application-level and database-level signals.
Key LoadForge metrics to review
In LoadForge’s real-time reporting, pay close attention to:
- Average response time
- P95 and P99 latency
- Requests per second
- Error rate
- Active users over time
- Endpoint-level performance breakdown
Average response time alone is not enough. Database bottlenecks often show up first in tail latency, so P95 and P99 matter much more.
Mapping API symptoms back to database issues
Here’s how common load test results often map to database problems:
High latency but low error rate
This often suggests:
- Slow queries
- Missing indexes
- Inefficient ORM-generated SQL
- Large scans or aggregations
- Disk I/O contention
Sudden spike in 5xx errors
This may indicate:
- Database connection exhaustion
- Query timeouts
- Deadlocks
- Failover instability
- Overloaded read replicas
Good read performance but poor write performance
This can point to:
- Lock contention
- Index maintenance overhead
- Transaction log saturation
- Replication pressure
- Hot partitions
Latency increases sharply at a specific concurrency level
This usually means you found a capacity threshold such as:
- Maxed-out connection pools
- CPU saturation
- Buffer cache limits
- Disk queue buildup
- Application worker exhaustion waiting on the database
Correlate with database monitoring
For the most useful analysis, combine LoadForge results with database telemetry from your platform or observability stack. Correlate test timing with:
- Slow query logs
- Execution plans
- Lock wait times
- Replication lag
- Cache hit ratio
- CPU spikes
- Storage throughput
- Connection counts
Because LoadForge supports distributed testing from global test locations, you can also determine whether latency is truly database-related or partly network-related across regions.
Performance Optimization Tips
Once your load test identifies bottlenecks, these are the most common ways to improve database performance.
Optimize queries first
Before scaling infrastructure, inspect the slowest queries:
- Add missing indexes
- Remove unnecessary joins
- Avoid
SELECT * - Limit result sizes
- Use pagination
- Rewrite ORM queries that generate inefficient SQL
- Precompute expensive aggregates where possible
Tune connection handling
A frequent cause of poor database performance testing results is connection mismanagement:
- Use connection pooling
- Set sane pool sizes
- Avoid opening new connections per request
- Monitor pool wait times
- Separate read and write pools if your architecture supports it
Improve indexing carefully
Indexes speed reads but add write overhead. For write-heavy systems:
- Keep only necessary indexes
- Avoid redundant indexes
- Revisit composite index order
- Validate that indexes match actual query patterns
Reduce transaction scope
Long transactions increase lock contention. Try to:
- Keep transactions short
- Avoid reading large datasets inside transactions
- Batch writes carefully
- Use idempotency keys for retries
Partition or shard when needed
For very large datasets or high-ingestion systems:
- Partition time-series tables
- Distribute tenants evenly
- Avoid hot keys
- Route reads to replicas where appropriate
Test with realistic scale
Performance testing results are only useful if the workload resembles production:
- Use realistic data volumes
- Simulate real user behavior
- Weight tasks according to actual usage
- Include both reads and writes
- Run sustained tests, not just short spikes
With LoadForge, you can scale these tests using cloud-based infrastructure and automate them in CI/CD pipelines so regressions are caught before release.
Common Pitfalls to Avoid
Database load testing is easy to get wrong if the test setup is unrealistic. Avoid these common mistakes.
Testing only the homepage or health check
If your goal is database performance testing, don’t focus on endpoints that barely touch the database. Target the flows that actually matter:
- Search
- Checkout
- Reporting
- User history
- Inventory lookups
- Bulk writes
Using tiny datasets
A query that performs well on 10,000 rows may fail badly on 100 million. Always test with representative data sizes.
Ignoring authentication and session flows
Authenticated requests often trigger user lookups, permissions checks, and tenant scoping. Excluding auth can understate database load.
Running only read tests
Many databases fail under writes, not reads. Include inserts, updates, transactions, and batch operations in your stress testing plan.
Not isolating caches
Caching can hide database issues. Decide whether you want to test:
- Warm cache behavior
- Cold cache behavior
- Cache bypass scenarios
Each tells you something different.
Overlooking background jobs
Your database may also be handling:
- ETL jobs
- Search indexing
- Notifications
- Scheduled reports
- CDC pipelines
If these run in production, include them in your test window or account for their impact.
Testing production without safeguards
Never run aggressive stress testing against production unless you have clear approval, protections, and rollback plans. Use staging or a controlled environment whenever possible.
Not ramping gradually
Jumping immediately to very high concurrency makes it harder to find the exact threshold where performance degrades. Prefer step-wise ramp-up tests to identify capacity limits cleanly.
Conclusion
Load testing databases is one of the most effective ways to prevent slow queries, transaction bottlenecks, lock contention, and scaling surprises before they reach production. Whether you’re working with SQL or NoSQL systems, the right approach is to simulate realistic application behavior with representative read, write, and aggregation workloads.
Using Locust scripts on LoadForge, you can build practical database load testing scenarios, run them at scale with distributed cloud infrastructure, and analyze results through real-time reporting. You can also integrate these tests into your CI/CD pipeline so performance testing becomes a repeatable part of every release.
If you want to uncover database bottlenecks early and validate performance with confidence, try LoadForge and start building your first database load test today.
LoadForge Team
LoadForge is a load and performance testing platform built on Locust. Our team has been shipping load tests against production systems since 2018, and we write these guides from real customer engagements.
Related guides
Keep going with more guides from the same category.

Cassandra Load Testing with LoadForge
Run Cassandra load tests with LoadForge to measure write throughput, query latency, and distributed database resilience.

CockroachDB Load Testing with LoadForge
Load test CockroachDB with LoadForge to evaluate SQL performance, transaction latency, and horizontal scalability.

DynamoDB Load Testing with LoadForge
Load test DynamoDB with LoadForge to validate read and write capacity, throttling behavior, and performance at scale.