Database Query Load Test: Find Slow Queries

Load test the API endpoints behind your heaviest queries and read p95 per endpoint against user count to find the query that stops scaling. Config inside.


Find the query that stops scaling before your users do

A query that takes 40 ms alone takes 4 seconds when 200 requests want the same index at once, and nothing in a unit test shows that. This template drives the endpoints behind your heaviest queries (search, listing with filters, reporting, anything with a JOIN across large tables) at a rising user count and reads response time per endpoint, so the slow query shows up as the endpoint whose p95 climbs first.

Configuration

SettingValueWhy
Virtual users300Enough to exhaust a default connection pool (often 100) and expose lock contention.
Duration15 minutesQuery caches and buffer pools warm in the first minutes; the steady state is what matters.
Ramp-up180 s in 6 steps50 users every 30 seconds; compare each endpoint’s p95 across the six levels.
Requests3 to 6 read endpoints with varied parametersUse different search terms, page numbers and filters per request, or you will test the query cache.
WritesOptional: one write endpoint at 10% of trafficWrites take locks; a read-only test misses the contention that matters.
Think time0.5 to 2 sEnough to keep the load realistic; the target is the database, not the web tier.

Run this templateOpens the cloud test form with these values filled in. Free plan runs it at the free user limit; sign in or create a free account first.

The button prefills users, duration and ramp-up. Add the endpoints with varied query parameters (a random variable per request gives each call a different id or page), then start.

What to read in the results

  • p95 per endpoint at each user level. Filter the results by request. The endpoint whose p95 rises first and steepest is the query to fix; the ones that stay flat share its connection pool and will suffer next.
  • Throughput plateau versus pool size. If requests per second flatten at a number close to pool size divided by average query time, the pool is the limit, not the database.
  • Timeouts and 5xx. Timeouts under load with no errors at low load are lock waits or pool exhaustion. Match the timestamps against slow-query logs; the results page gives you the exact minute.

Pass/fail thresholds for this template

ThresholdTargetWhat a breach means
p95 response time< 500 msA query-backed endpoint is queueing at this user count.
Error rate< 0.5%Connections or statements are timing out.
Throughput> your target requests/sThe pool or the database is the bottleneck below your target.

Save them on the test; after an index or a pool change, the Trend tab shows whether it helped.

The same scenario as a k6 script

As a k6 script, with parameters drawn from a small list so every request hits a different key. Replace the endpoints with yours.

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [{ duration: '3m', target: 300 }, { duration: '12m', target: 300 }],
  thresholds: { http_req_duration: ['p(95)<500'], http_req_failed: ['rate<0.005'] },
};

const BASE = 'https://api.example.com';
const terms = ['boots', 'jacket', 'lamp', 'desk', 'phone', 'tent'];
const pick = (a) => a[Math.floor(Math.random() * a.length)];

export default function () {
  check(http.get(`${BASE}/search?q=${pick(terms)}&page=${1 + Math.floor(Math.random() * 20)}`), { 'search 200': (r) => r.status === 200 });
  check(http.get(`${BASE}/orders?status=shipped&from=2026-01-01&page=${1 + Math.floor(Math.random() * 50)}`), { 'orders 200': (r) => r.status === 200 });
  if (Math.random() < 0.1) http.post(`${BASE}/reviews`, JSON.stringify({ product: pick([11, 12, 13]), rating: 5 }), { headers: { 'Content-Type': 'application/json' } });
  sleep(0.5 + Math.random() * 1.5);
}

When to run it

  • After adding a feature that introduces a new filter, sort or report.
  • Before a data migration that grows the largest tables.
  • After changing pool size, indexes or the database tier to measure rather than assume.

FAQ on database load testing

Why test through the API instead of the database directly?

Because that is how production load arrives: through the pool, the ORM and the cache, with real parameter distributions. A SQL benchmark tool tells you the query cost; this tells you the user-facing cost.

How do I avoid testing the query cache?

Vary the parameters. The cloud test’s random variables give each request a different numeric value (an id, a page, a price range); the k6 script above draws from a list. Identical requests measure the cache, not the query.

The first minute is slow and then it gets fast. Which number is real?

The steady state. Cold buffer pools and JIT plans inflate the first minute; read p95 from minute three onward, or set the ramp long enough to warm up.

Can I run this against production?

Read-only endpoints at a quiet hour, yes, with a user count below the plateau you found on staging. Anything with writes belongs on staging.

How fast is your website?

Elevate its speed and SEO seamlessly with our Free Speed Test.

Outgrown your testing tools?

Load test websites and APIs from 25+ cloud regions, monitor page speed and uptime, and get AI analysis that explains your results in plain English.Start for free
jmeter cloud load testing tool

Free Website Speed Test

Analyze your website's load speed and improve its performance with our free page speed checker.

×