Skip to main content

📊 Performance Testing Tools

Your test suite proves the app is correct. Performance testing proves it stays correct under pressure — when a hundred users, then a thousand, then ten thousand hit it at once. In this lesson you'll meet the tools that generate that pressure and read the vital signs that come back.

Week 12 · Thursday: Performance Testing · Lecture 1

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Distinguish the four load-test types — load, stress, spike, and soak — and know when each applies
  • Read the metrics that matter: throughput (RPS), latency percentiles (p50/p95/p99), error rate, and concurrency
  • Explain why the average response time lies and percentiles tell the truth
  • Compare the major tools — k6, Artillery, JMeter, Apache Bench, hey, and Lighthouse — by protocol, scripting, and workflow
  • Choose the right tool for a given scenario and justify the choice

Estimated Time: 55 minutes

Practice: Match tools to scenarios, run a mental spike test, and pick metrics for a checkout API.

In This Lesson

Why Performance Testing?

Functional testing answers "does it work?" Performance testing answers a harder question: "does it still work when everyone shows up at once?" A checkout endpoint that responds in 40 ms with one user might crawl to 4 seconds — or fall over entirely — under the traffic of a flash sale. You want to discover that on a Tuesday afternoon in a test environment, not at 9 PM on Black Friday.

The numbers make the case. Public engineering write-ups have long reported the same pattern: every extra 100 ms of latency measurably shaves conversions, and users abandon pages that take more than a few seconds to respond. Slow isn't a cosmetic problem — it's a revenue problem, a retention problem, and increasingly an SEO problem.

graph TD A["Performance testing"] --> B["Load testing"] A --> C["Stress testing"] A --> D["Spike testing"] A --> E["Soak / endurance"] A --> F["Frontend performance"] B --> G["k6"] B --> H["Artillery"] B --> I["JMeter"] C --> G C --> I D --> G D --> H E --> G E --> I F --> J["Lighthouse"] F --> K["WebPageTest"]

📖 Backend vs. frontend performance

Two different worlds share the word "performance." Backend / load testing asks how many requests per second your server sustains and how latency grows with concurrency — that's k6, Artillery, and JMeter. Frontend performance asks how fast a page paints and becomes interactive in a real browser — that's Lighthouse and Core Web Vitals. This week focuses on the backend side, but a complete picture needs both.

The Four Load-Test Types

"Run a load test" is like "cook dinner" — it hides several very different activities. The shape of the traffic you generate defines what you're actually testing. There are four classic patterns, and they answer four different questions.

Four load-test traffic shapes: load ramps and holds, stress climbs past capacity, spike jumps suddenly, soak holds for hours Load Stress Spike Soak / Endurance
Each test is a different shape of virtual-user traffic over time. The x-axis is time; the y-axis is concurrent load.
TypeTraffic shapeQuestion it answers
LoadRamp to expected peak, then holdDoes it meet its SLA under normal busy conditions?
StressKeep increasing past capacityWhere does it break, and does it fail gracefully?
SpikeSudden jump, then dropCan it survive a flash crowd (a viral post, a sale opening)?
Soak / enduranceModerate load held for hoursDo memory leaks, connection exhaustion, or slow degradation appear over time?

✅ A soak test catches what a load test can't

A leak that grows the heap by a few megabytes per minute is invisible in a 5-minute load test — the app looks perfectly healthy. Run the same moderate load for two hours and the process slowly swells until the garbage collector thrashes and latency climbs. Soak tests exist precisely to surface these slow-burn failures.

The Metrics That Matter

A load test spits out a wall of numbers. Four of them carry almost all the signal.

Throughput (requests per second)

Throughput, usually reported as RPS (requests per second), is how much work the system completes per unit of time. It's the "how many customers can we serve" number. Higher is better — up to the point where the server saturates and throughput flattens no matter how much load you add.

Latency — and why the average lies

Latency is how long a single request takes. The trap is reporting it as an average. Imagine 99 requests at 50 ms and one request at 5000 ms: the average is a comfortable-sounding ~100 ms, yet one in a hundred users waited five full seconds. Averages hide the users who suffer.

The fix is percentiles. The p95 latency is the value that 95% of requests came in under; p99 is the same for 99%. These describe the tail — the slow requests real users complain about.

Output — the same data, two ways to read it

Average latency  ...........  102 ms   ← looks great
p50 (median) ...............   48 ms
p95 ........................  240 ms
p99 ........................ 1900 ms   ← 1 in 100 users waited ~2 s

Error rate

The percentage of requests that failed — timeouts, 5xx responses, dropped connections. Under load, an app often stays fast right up until it starts failing. A rising error rate is frequently the first sign you've hit capacity. Anything above ~1% usually deserves immediate attention.

Concurrency

Concurrency is how many requests are in flight at the same moment — the pressure you're applying. Load tools express it two ways: a fixed number of virtual users (VUs) each looping, or an arrival rate (new users per second). The distinction matters: arrival-rate models are closer to how real traffic behaves, because real users don't wait politely for the previous one to finish.

⚠️ Report the tail, not just the middle

When someone asks "how fast is the API?", answer with p95 and p99, not the average. If you only remember one lesson from this section, make it this: optimize the tail. Median performance keeps most users happy; tail performance decides whether your worst-served users leave.

The Tool Landscape

Dozens of tools generate load. For a JavaScript full-stack developer, three matter most, plus a handful of lightweight helpers.

Grafana k6 — the modern default

k6 is a load-testing tool you script in JavaScript but that runs on a fast Go engine. That combination — familiar language, high performance, tiny resource footprint — has made it the de-facto modern standard. Tests are code, so they live in your repo, review in pull requests, and run in CI.

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

// Options define the traffic shape AND the pass/fail thresholds (the SLA).
export const options = {
  stages: [
    { duration: '30s', target: 20 },  // ramp up to 20 virtual users
    { duration: '1m',  target: 20 },  // hold at 20 for a minute
    { duration: '30s', target: 0 },   // ramp back down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'], // 95% of requests must finish under 500 ms
    http_req_failed:   ['rate<0.01'], // error rate must stay under 1%
  },
};

export default function () {
  const res = http.get('https://api.example.com/products');
  check(res, {
    'status is 200':     (r) => r.status === 200,
    'body is not empty':  (r) => r.body.length > 0,
  });
  sleep(1); // "think time" — pause like a real user before the next request
}

Why it matters: those thresholds turn a load test into a gate. If p95 exceeds 500 ms or errors exceed 1%, k6 exits non-zero and your CI build fails — exactly the SLA-as-code behavior you want.

Artillery — YAML scenarios, Node-native

Artillery is a Node.js load tester where you describe traffic in readable YAML and drop into JavaScript only when you need custom logic. It excels at multi-step user journeys and integrates naturally with a Node stack. We devote the next lesson entirely to it.

config:
  target: "https://api.example.com"
  phases:
    - duration: 60
      arrivalRate: 5
      rampTo: 50
      name: "Warm up"
scenarios:
  - name: "Browse products"
    flow:
      - get:
          url: "/products"
          expect:
            - statusCode: 200
      - think: 3
      - get:
          url: "/products/{{ $randomNumber(1, 100) }}"

Apache JMeter — the veteran

JMeter is a mature, Java-based tool with a GUI and support for a huge range of protocols (HTTP, JDBC, JMS, FTP, and more). It's powerful and battle-tested, but heavier on resources and less friendly to a code-review workflow than k6 or Artillery. You'll meet it in enterprise shops and legacy pipelines.

Featurek6ArtilleryJMeter
ScriptingJavaScriptYAML + JavaScriptGUI / XML + Groovy
EngineGo (fast, low RAM)Node.jsJVM (heavier)
SLA thresholdsBuilt-inBuilt-inVia plugins
CI/CD fitExcellentVery goodWorkable
Learning curveModerateGentleSteep
Best forDev-owned tests in CINode teams, journeysMany protocols, enterprise

Quick Benchmarking

Sometimes you don't need a scripted scenario — you just want a fast pulse-check on a single endpoint. Two tiny command-line tools cover that.

Apache Bench (ab) and hey

ab ships with Apache and has measured HTTP servers for decades. hey is a modern Go rewrite with HTTP/2 support. Both fire a burst of requests and print throughput and latency percentiles in seconds.

# Apache Bench: 1000 requests, 50 at a time
ab -n 1000 -c 50 https://api.example.com/products

# hey: 2000 requests, 50 concurrent, with an auth header
hey -n 2000 -c 50 -H "Authorization: Bearer token123" https://api.example.com/users

💡 When "quick" is the right answer

Reach for ab or hey to sanity-check a single URL after a config change, or to compare two server setups head-to-head. Reach for k6 or Artillery the moment you need multi-step journeys, dynamic data, per-endpoint metrics, or SLA thresholds that fail a build.

A note on frontend performance

Load tools measure the server. To measure the browser experience — how fast a page paints and becomes interactive — use Lighthouse (built into Chrome DevTools and runnable in CI). It scores Core Web Vitals like Largest Contentful Paint and Cumulative Layout Shift. Different question, different tool, both worth automating.

Choosing the Right Tool

There's no universally "best" tool — only the best fit for a scenario. Walk through five questions and the choice usually makes itself.

graph TD A["What are you testing?"] --> B{"Single URL,
quick check?"} B -->|Yes| C["Apache Bench or hey"] B -->|No| D{"Browser paint
and interactivity?"} D -->|Yes| E["Lighthouse"] D -->|No| F{"Team stack
and workflow?"} F -->|JS in CI, code-first| G["k6"] F -->|Node, YAML journeys| H["Artillery"] F -->|Many protocols, enterprise| I["JMeter"]
If you need…Reach for…
A fast pulse-check on one endpointApache Bench, hey
Code-first load tests that gate CI on an SLAk6
Readable multi-step journeys in a Node projectArtillery
Broad protocol support in an enterprise settingJMeter
Frontend / Core Web Vitals scoringLighthouse

Practice & Quiz

🏋️ Exercise 1: Match the test type

Goal: A teammate lists four worries about the new booking API. Match each worry to the load-test type that addresses it.

  1. "Will it survive the moment tickets go on sale and everyone clicks at once?"
  2. "Does it hold up through our normal Friday-evening rush?"
  3. "At what traffic level does it actually fall over?"
  4. "Does it stay healthy running all weekend, or does something slowly leak?"
💡 Hint

Think about the shape of the traffic each worry describes: a sudden jump, a steady expected peak, an ever-climbing ramp, or a long steady hold.

✅ Solution

1 → Spike test (sudden flash crowd). 2 → Load test (expected peak, held). 3 → Stress test (climb until it breaks). 4 → Soak / endurance test (long steady hold reveals leaks).

🏋️ Exercise 2: Read the tail

Goal: A run reports average 90 ms, p50 45 ms, p95 210 ms, p99 2400 ms, error rate 0.3%. Your SLA is "p95 under 300 ms and p99 under 1000 ms." Does it pass? What's the story?

✅ Solution

It fails. p95 (210 ms) is comfortably within its 300 ms budget, but p99 (2400 ms) blows past the 1000 ms budget. The healthy average and median hide a slow tail: roughly 1 in 100 requests takes ~2.4 seconds. That's a real user problem the average completely masks — investigate what those slowest 1% have in common (a cold cache? an unindexed query path?).

🎯 Quick Quiz

Question 1: Which metric best reveals the experience of your worst-served users?

Question 2: You want to know if a memory leak degrades the app over hours of steady traffic. Which test type?

Question 3: Which tool lets you write load tests in JavaScript, run them on a fast Go engine, and fail a CI build on an SLA threshold?

Best Practices & Pitfalls

✅ Do

  • Report percentiles (p50/p95/p99), not just the average
  • Watch the error rate — apps often fail before they slow down
  • Model traffic with an arrival rate when you can — it mirrors real users
  • Test against a production-like environment, with realistic data volumes
  • Store load tests in your repo and run them in CI, like any other test

❌ Don't

  • Judge performance by the average alone — it hides the tail
  • Load-test production without permission and a plan (you can cause a real outage)
  • Trust a 2-minute run to reveal leaks — that's what soak tests are for
  • Reach for a heavyweight tool when ab or hey answers the question in seconds

⚠️ Your load generator can be the bottleneck

If the machine running the test is maxed out on CPU or network, its own limits will masquerade as the target's limits. This is why k6's low footprint matters: you can push far more load per machine before the generator becomes the constraint. Always confirm the generator has headroom before trusting the numbers.

Summary

🎉 Key Takeaways

  • There are four load-test types — load (expected peak), stress (find the break), spike (flash crowd), soak (slow-burn leaks)
  • The metrics that matter are throughput, latency percentiles, error rate, and concurrency
  • The average latency lies; report p95 and p99 to see the tail real users feel
  • k6 (JS on a Go engine) is the modern default; Artillery shines for Node journeys; JMeter covers enterprise protocols
  • Use ab/hey for quick single-URL checks and Lighthouse for frontend Core Web Vitals

📚 Additional Resources

🚀 What's Next?

You know the landscape and the vocabulary. Next we go hands-on with one tool end to end — installing it, scripting realistic multi-step journeys, feeding it dynamic data, and reading its report: Load Testing with Artillery.

🎉 Well measured!

You can now name the four test types, read a percentile table without being fooled by the average, and pick the right tool for the job. That's the foundation everything this week builds on.