ESC
Type to search guides, tutorials, and reference documentation.
Verified by Garnet Grid

Performance Testing and Load Testing: Finding Breaking Points Before Users Do

Design and execute performance tests that reveal your system's limits before production traffic does. Covers load testing strategies, tool selection, realistic traffic patterns, baseline establishment, bottleneck identification, and the reporting process that turns performance data into engineering decisions.

Every system has a breaking point. Performance testing finds it before your users do. The goal is not to prove your system is fast — it is to discover exactly where, when, and how it fails under load. A system that handles 100 requests per second gracefully but collapses at 150 is more dangerous than one that degrades slowly, because the cliff edge is invisible until you hit it.


Types of Performance Tests

TypePurposeDurationLoad Pattern
Load testVerify expected traffic levels10-30 minRamp to expected peak
Stress testFind breaking point30-60 minRamp beyond expected peak
Soak testFind memory leaks, resource exhaustion4-24 hoursSustained moderate load
Spike testVerify sudden traffic burst handling5-15 minSudden jump, then drop
Capacity testDetermine maximum throughput30-60 minProgressive increase until failure

Load Test Design

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

export const options = {
  stages: [
    { duration: '2m', target: 50 },   // Ramp up to 50 users
    { duration: '5m', target: 50 },   // Hold at 50 users
    { duration: '2m', target: 200 },  // Ramp up to 200 users
    { duration: '5m', target: 200 },  // Hold at 200 users (expected peak)
    { duration: '2m', target: 400 },  // Stress: double expected peak
    { duration: '5m', target: 400 },  // Hold under stress
    { duration: '2m', target: 0 },    // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500', 'p(99)<1000'],  // 95th < 500ms, 99th < 1s
    http_req_failed: ['rate<0.01'],                   // Error rate < 1%
    http_reqs: ['rate>100'],                           // Throughput > 100 req/s
  },
};

export default function () {
  // Simulate realistic user behavior
  const loginRes = http.post('https://api.example.com/auth/login', JSON.stringify({
    email: `user_${__VU}@test.com`,
    password: 'testpassword',
  }), { headers: { 'Content-Type': 'application/json' } });

  check(loginRes, {
    'login successful': (r) => r.status === 200,
  });

  const token = loginRes.json('token');
  const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };

  // Browse products (most common action)
  const products = http.get('https://api.example.com/products?page=1', { headers });
  check(products, {
    'products loaded': (r) => r.status === 200,
    'has results': (r) => r.json('data').length > 0,
  });

  sleep(Math.random() * 3 + 1); // Think time: 1-4 seconds

  // Add to cart (less frequent)
  if (Math.random() < 0.3) {
    http.post('https://api.example.com/cart', JSON.stringify({
      productId: 'prod_001',
      quantity: 1,
    }), { headers });
  }

  // Checkout (rare)
  if (Math.random() < 0.05) {
    http.post('https://api.example.com/checkout', JSON.stringify({
      paymentMethod: 'tok_test',
    }), { headers });
  }

  sleep(Math.random() * 2 + 1);
}

Tool Comparison

ToolLanguageProtocolBest For
k6JavaScriptHTTP, WebSocket, gRPCDeveloper-friendly, CI integration
LocustPythonHTTPPython teams, custom scenarios
GatlingScala/JavaHTTPJVM ecosystems, detailed reports
JMeterJava (GUI)HTTP, JDBC, JMSEnterprise, protocol variety
ArtilleryJavaScriptHTTP, WebSocketQuick tests, YAML config
wrk/wrk2C (CLI)HTTPRaw throughput benchmarks

Key Metrics to Measure

MetricWhat It Tells YouTarget
Throughput (req/s)How many requests your system handles> expected peak × 2
Response time (p50)Typical user experience< 200ms
Response time (p95)Experience for most users< 500ms
Response time (p99)Worst-case common experience< 1000ms
Error ratePercentage of failed requests< 0.1% under normal load
CPU utilizationServer compute saturation< 70% at expected peak
Memory utilizationMemory pressure< 80%, no growth over time
Database connectionsConnection pool exhaustion< 80% of pool size
Queue depthBackpressureNot growing under sustained load

Bottleneck Identification

Systematic approach to finding bottlenecks:

  1. Run load test, observe where degradation begins
  2. Identify the constraint:

     CPU bound?        → Profile code, optimize hot paths
     Memory bound?     → Find memory leaks, reduce allocations
     I/O bound?        → Optimize queries, add caching, async I/O
     Connection bound? → Increase pool size, add connection pooling
     Network bound?    → Reduce payload size, compression
     Database bound?   → Add indexes, optimize queries, cache results

  3. Fix the constraint, re-test
  4. The next bottleneck will appear at a higher load level
  5. Repeat until you exceed 2x expected peak

CI Integration

# Run performance test on every release candidate
name: Performance Test

on:
  push:
    branches: [release/*]

jobs:
  load-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run k6 load test
        uses: grafana/k6-action@v0.3.1
        with:
          filename: tests/performance/load-test.js
        env:
          K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}

      - name: Check results
        run: |
          # Fail the pipeline if performance regressed
          if [ "$K6_EXIT_CODE" -ne 0 ]; then
            echo "Performance test failed: thresholds not met"
            exit 1
          fi

Anti-Patterns

Anti-PatternProblemFix
Testing from the same networkHides latency issuesTest from external location or cloud
Uniform request patternsUnrealistic (real traffic is bursty)Add think time, weighted scenarios
No baselineCannot tell if performance regressedEstablish baseline, run regularly
Testing only happy pathMisses error handling performanceInclude error scenarios, edge cases
One-time performance testRegressions slip in over timeRun on every release, track trends

Implementation Checklist

  • Choose a load testing tool and write tests alongside application code
  • Model realistic user scenarios with weighted actions and think time
  • Establish performance baselines: document current p50, p95, p99, throughput
  • Run load tests in CI on release branches — block deployment if thresholds fail
  • Test at 2x expected peak traffic to maintain headroom
  • Run soak tests (4+ hours) quarterly to catch memory leaks
  • Monitor server resources (CPU, memory, connections) during tests
  • Identify and fix the top bottleneck, then re-test for the next one
  • Track performance trends over time — detect gradual regressions
  • Include authentication and session management in test scenarios
Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →