Performance testing your API with Grafana K6

Introduction
Functional correctness and performance under load are two entirely different questions, and a passing integration test suite answers only the first one. Grafana k6 answers the second - a scriptable load-testing tool that simulates many concurrent virtual users hitting your API and reports back exactly where it starts to buckle.
🎬 Watch the full video here:
A k6 test is a JavaScript file, not a GUI configuration
import http from 'k6/http'
import { check, sleep } from 'k6'
export const options = {
vus: 50,
duration: '30s',
}
export default function () {
const res = http.get('https://localhost:5001/api/products')
check(res, { 'status is 200': (r) => r.status === 200 })
sleep(1)
}
vus: 50 runs 50 virtual users concurrently, each looping the default function for the test's duration. Being a script rather than a recorded-and-replayed GUI flow means the test lives in source control and reviews like any other code.
Reading the results: percentiles matter more than averages
k6's summary reports response time as percentiles, not just an average - and the average is close to useless here:
http_req_duration..............: avg=45ms min=12ms med=38ms p(90)=78ms p(95)=112ms
An average of 45ms can hide a p95 of over 100ms - meaning one in twenty requests is more than twice as slow as the "typical" number suggests. Real users experience the tail, not the average, which is why p95/p99 are the numbers worth setting a target against, not the mean.
Ramping load instead of a flat spike
A flat vus: 50 for the whole test doesn't show where the API starts to degrade - a staged ramp does:
export const options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 100 },
{ duration: '30s', target: 0 },
],
}
Watching latency and error rate climb as target ramps from 20 to 100 virtual users identifies the actual breaking point, rather than just confirming the API survives one fixed load level.
Load test vs. stress test vs. spike test
These get used interchangeably but answer different questions: a load test confirms the API meets its performance target under expected traffic; a stress test pushes well past expected traffic to find where it actually breaks; a spike test slams from low to very high load almost instantly to see how the API (and any auto-scaling behind it) reacts to a sudden burst. Picking the wrong one for the question you're actually asking gives you numbers that don't answer it.
Watching the .NET side, not just k6's output
k6 tells you what the client saw. Correlating that with the API's own metrics (CPU, memory, database connection pool usage, GC pauses) during the same run is what tells you why latency degraded - a connection pool exhausted under 100 concurrent users points at a completely different fix than CPU saturation would.
Common pitfalls
- Reading only the average response time and missing that p95/p99 tell a very different story.
- Testing against a database seeded with far less data than production, making the results meaningless.
- Running the load test from the same machine as the API, so the test tool itself competes for CPU with the thing it's measuring.
- Not correlating client-side k6 results with server-side metrics during the same run.
- Treating a stress test's breaking point as "the API's capacity" without also considering acceptable latency at that point, not just whether it stayed up.
Key Takeaways
- k6 tests are JavaScript, living in source control, not a recorded GUI flow.
- Percentiles (p95, p99), not the average, tell you what real users actually experience under load.
- A staged ramp shows where performance degrades, not just whether the API survives one fixed load level.
- Load, stress, and spike tests answer different questions - pick the one that matches what you're actually trying to learn.
- Correlate k6's client-side numbers with the API's own server-side metrics to find the actual bottleneck.
Get the Full Source Code
The complete runnable solution - the k6 test scripts (flat, ramped, and spike variants) and the .NET API under test - is available to Patreon supporters. If you want the scripts ready to point at your own API instead of rebuilding them from the walkthrough above, you can find it on Patreon.