LoadForge LogoLoadForge

How to Load Test Databases with LoadForge

How to Load Test Databases with LoadForge

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.

python
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_products runs most often
  • product_search is moderately frequent
  • order_history is 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.

python
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.

python
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.

python
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.

Try LoadForge free for 7 days

Set up your first load test in under 2 minutes. No commitment.