Performance Testing Basics for Everyday QA

Performance testing isn't just for specialists. Learn when QA engineers should care about performance, how to run smoke performance tests with k6, and how to think about latency and error budgets.

Introduction — Performance Is a Feature

Performance testing often feels like someone else’s job. There’s a dedicated performance team, or a DevOps engineer who handles load testing before big launches, or it’s just not tested at all until production slows to a crawl.

But performance is not a separate concern — it’s a feature. A feature that works correctly but takes 8 seconds to load is a broken feature. Users don’t distinguish between “the function is correct” and “the function is unusably slow.” They experience the product as slow, and slow products lose users.

This post is for everyday QA engineers who want to integrate basic performance awareness into their testing practice without becoming full-time performance specialists. You’ll learn:

  • When QA should care about performance (and when to escalate to specialists)
  • How to run smoke performance tests with k6
  • How to think about latency and error budgets
  • How to interpret basic performance metrics

This is not a deep dive into distributed load testing, percentile analysis, or queueing theory. It’s a practical introduction to performance testing as a QA discipline.


When QA Should Care About Performance

Not every feature requires dedicated performance testing. Focus your effort where it matters most.

High-Priority Scenarios

Test performance for:

  1. API endpoints used in tight loops — If a frontend makes 50 calls to an API during page load, even 100ms per call becomes 5 seconds of blocking time.
  2. User-facing operations with SLAs — Payment processing, search results, checkout flows. These often have explicit latency targets (e.g., “search results in <200ms”).
  3. High-traffic endpoints — Login, homepage, feed generation. Small regressions here affect many users.
  4. Database-heavy operations — Reports, dashboards, exports. These are prone to N+1 queries, missing indexes, and cartesian joins.
  5. After major refactors — Replacing a database, migrating to a new API, changing caching strategies. Validate that performance didn’t regress.

When to Escalate to Specialists

Escalate to a dedicated performance team when:

  • You need distributed load testing with thousands of concurrent users across multiple regions.
  • You’re testing sustained load over hours or days (soak testing, stress testing).
  • The problem requires deep profiling of database query plans, memory leaks, or garbage collection tuning.
  • You need to model capacity planning for future growth (e.g., “can we handle 10× traffic?”).

As a QA engineer, your job is to catch obvious regressions and establish smoke-level performance baselines. Specialists handle the deep analysis.

:::info[Smoke Performance Testing] Smoke performance testing means running a lightweight load test to catch obvious problems: a query that takes 5 seconds instead of 50ms, an endpoint that crashes under 10 concurrent users, or a memory leak in a background job. You’re not modeling production load — you’re catching show-stoppers before they reach production. :::


Performance Metrics That Matter

Before writing tests, understand the metrics you’re measuring.

Latency (Response Time)

The time between sending a request and receiving a response. Measured in milliseconds (ms).

Why it matters: Latency directly affects user experience. A 2-second page load feels slow. A 200ms page load feels instant.

How to measure: Record response time for each request. Report percentiles, not just averages.

Percentiles (p50, p95, p99)

Percentiles show the distribution of latency:

  • p50 (median) — 50% of requests are faster than this. The typical user experience.
  • p95 — 95% of requests are faster than this. 5% of users see worse performance.
  • p99 — 99% of requests are faster than this. 1% of users see worse performance.

Why percentiles matter: Averages hide outliers. If 99 requests take 100ms and 1 request takes 10 seconds, the average is 200ms — but one user had a terrible experience.

Throughput (Requests per Second)

How many requests your system can handle per second under load.

Why it matters: Throughput reveals capacity limits. If your API can only handle 50 requests/second and you expect 500/second in production, you have a problem.

Error Rate

Percentage of requests that fail (HTTP 5xx, timeouts, connection errors).

Why it matters: High load often causes failures before it causes slowness. An endpoint that works fine with 10 users might start returning 503 errors with 100 users.

:::tip[Focus on Percentiles, Not Averages] A p95 latency of 500ms means 95% of users wait 500ms or less. The remaining 5% might wait 2 seconds, 5 seconds, or longer. Averages hide these outliers. Always report p95 or p99 for user-facing endpoints. :::


Running Smoke Performance Tests with k6

k6 is an open-source, developer-friendly performance testing tool. Scripts are written in JavaScript, execution is fast, and CI/CD integration is straightforward.

Installation

# macOS
brew install k6

# Windows
choco install k6

# Linux
sudo apt-get install k6

Or download from k6.io.

A Simple Smoke Test

Create smoke-test.js:

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

export let options = {
  vus: 10,        // 10 virtual users
  duration: '30s', // Run for 30 seconds
};

export default function () {
  let res = http.get('https://api.example.com/products');
  
  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });
  
  sleep(1);
}

Run:

k6 run smoke-test.js

Output:

     data_received..................: 1.2 MB  40 kB/s
     data_sent......................: 8.5 kB  283 B/s
     http_req_duration..............: avg=245ms min=120ms med=230ms max=680ms p(90)=380ms p(95)=450ms
     http_reqs......................: 300     10/s
     checks.........................: 100%

What this tells you:

  • http_req_duration — Average 245ms, p95 450ms. Most requests are fast, but 5% take up to 680ms.
  • http_reqs — 10 requests/second sustained for 30 seconds.
  • checks — 100% of requests passed the status is 200 and response time < 500ms checks.

If this test fails (e.g., p95 > 1 second, or checks drop below 100%), you’ve caught a regression.


Designing Realistic Load Scenarios

Smoke tests are intentionally lightweight. For more realistic scenarios, model actual user behavior.

Example: E-Commerce Checkout Flow

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

export let options = {
  stages: [
    { duration: '1m', target: 20 },  // Ramp up to 20 users over 1 min
    { duration: '3m', target: 20 },  // Hold 20 users for 3 min
    { duration: '1m', target: 0 },   // Ramp down to 0 users
  ],
};

export default function () {
  // 1. Browse products
  let productsRes = http.get('https://api.example.com/products');
  check(productsRes, { 'products status 200': (r) => r.status === 200 });
  sleep(2);

  // 2. Add to cart
  let cartRes = http.post('https://api.example.com/cart', JSON.stringify({
    productId: 123,
    quantity: 1,
  }), { headers: { 'Content-Type': 'application/json' } });
  check(cartRes, { 'cart status 201': (r) => r.status === 201 });
  sleep(1);

  // 3. Checkout
  let checkoutRes = http.post('https://api.example.com/checkout', JSON.stringify({
    cartId: cartRes.json('cartId'),
  }), { headers: { 'Content-Type': 'application/json' } });
  check(checkoutRes, {
    'checkout status 200': (r) => r.status === 200,
    'checkout < 1s': (r) => r.timings.duration < 1000,
  });
  sleep(5);
}

This simulates a realistic user flow: browse → add to cart → checkout. The sleep() calls simulate think time (users don’t click instantly).


Latency Budgets — How Fast Is Fast Enough?

A latency budget is a target maximum response time for a given operation. It answers the question: “How fast is fast enough?”

Industry Benchmarks

OperationTarget LatencyRationale
API read (single record)<100msFeels instant. No perceptible delay.
API write (create/update)<300msAcceptable for user-initiated actions.
Search query<200msUsers expect instant search results.
Page load (server response)<500msAllows time for client-side rendering.
Background job (async)Seconds to minutesNot user-facing. Optimize for reliability over speed.

These are guidelines, not absolutes. A complex report might take 2 seconds and still be acceptable if users understand it’s a heavy operation.

Setting Your Own Latency Budget

  1. Measure current performance — Run k6 tests against your API. Record p95 latency.
  2. Define acceptable thresholds — “p95 < 500ms for all endpoints” or “checkout < 1 second”.
  3. Enforce in CI — Fail builds if latency exceeds the budget.

Example k6 threshold:

export let options = {
  thresholds: {
    'http_req_duration': ['p(95)<500'], // Fail if p95 > 500ms
    'http_req_failed': ['rate<0.01'],   // Fail if >1% of requests fail
  },
};

If these thresholds are violated, k6 exits with a non-zero code, failing the CI build.

:::warning[Don’t Test Against Production] Run performance tests against staging or a dedicated performance environment. Load testing production can cause real user impact and violate terms of service for third-party APIs. :::


Error Budgets — Balancing Speed and Reliability

An error budget is the maximum acceptable failure rate for a service over a time period.

Example: 99.9% Uptime SLA

99.9% uptime means 0.1% of requests can fail. Over a month with 10 million requests, you have a budget of 10,000 failed requests.

If you spend the entire budget in week one (a bad deploy, a database outage), you have no remaining tolerance for failures for the rest of the month. The team must prioritize stability over new features.

Error Budgets for Performance

Apply the same concept to latency. Define an error budget for slow requests:

  • “99% of requests must be faster than 500ms” means 1% can be slower.
  • If 5% of requests are slower than 500ms, you’ve exceeded the budget.

This prevents the “everything is fast except for these occasional 10-second outliers” problem.


Integrating Performance Testing Into Your Workflow

Strategy 1: Smoke Performance Tests in CI

Add a k6 smoke test to your CI pipeline. Run it on every PR for critical endpoints.

# .github/workflows/performance.yml
name: Performance Tests

on: [pull_request]

jobs:
  k6:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install k6
        run: |
          sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
          echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
          sudo apt-get update
          sudo apt-get install k6
      - name: Run smoke test
        run: k6 run tests/smoke-test.js

Strategy 2: Nightly Load Tests

Run heavier load tests nightly against staging. Report results to a dashboard (Grafana, Datadog).

Strategy 3: Pre-Release Performance Validation

Before major releases, run a full load test simulating expected production traffic. Validate that performance meets SLAs.


Common Performance Problems QA Can Catch

N+1 Query Problem

Your API returns a list of 100 users. For each user, it makes a separate database query to fetch their profile picture. That’s 101 queries (1 for the list + 100 for pictures).

How to detect: Run a k6 test with a small number of users. Check database query logs. If you see hundreds of queries for a single API call, you have an N+1 problem.

Solution: Use database joins or batch loading.

Missing Database Index

A search endpoint scans the entire users table (1 million rows) on every request because the email column isn’t indexed.

How to detect: Response time increases dramatically with data volume. A query that takes 50ms with 1,000 rows takes 5 seconds with 1 million rows.

Solution: Add a database index.

Uncontrolled Memory Growth

A background job processes 10,000 records and loads them all into memory at once. With 100,000 records, it runs out of memory.

How to detect: Run a load test with realistic data volume. Monitor memory usage. If memory grows unbounded, you have a leak or unbounded allocation.

Solution: Process data in batches.


Tools and Ecosystem

ToolUse CaseNotes
k6Smoke and load testingBest for API and microservice testing. Scripts in JavaScript.
Apache JMeterEnterprise load testingGUI-based. Mature. Supports many protocols.
GatlingScala-based load testingDeveloper-friendly. Excellent reports.
LocustPython-based load testingScripts in Python. Good for complex scenarios.
LighthouseFrontend performanceAudits page load, accessibility, SEO. Great for web performance.

For QA engineers, k6 is the best starting point: minimal setup, great CI/CD integration, and clear documentation.


Conclusion

Performance testing doesn’t require specialization to get started. As an everyday QA engineer, you can:

  • Run smoke performance tests with k6 to catch obvious regressions.
  • Define and enforce latency budgets for critical endpoints.
  • Think about percentiles (p95, p99) instead of averages.
  • Catch common problems like N+1 queries and missing indexes.

You don’t need to model production load, tune JVM garbage collection, or analyze queueing theory. Leave that to specialists. Focus on catching show-stoppers before they reach production.

Performance is a feature. Test it like one.

Action for this week: Identify the three most critical API endpoints in your application (e.g., login, search, checkout). Write a k6 smoke test for each one. Run it locally with 10 virtual users for 30 seconds. Record the p95 latency. If it’s over 500ms, investigate why.