I’ve been writing code for a bit now and not once have I thought about how it performs under load. Unit tests? Sure. Maybe an integration test when I’m feeling productive. But load testing? That was always someone else’s problem.
Why bother
Remember that Go API I built for this site? I deployed it, it worked, I moved on. Then one day I thought, what actually happens if more than 3 people use it at the same time? Unlikely for my portfolio, sure, but the thought stuck.
Also saw a couple job postings mentioning performance testing and k6, so I figured I’d kill two birds.
What is performance testing
Before touching any tools I spent some time reading up on what performance testing actually is. It’s not just “send a bunch of requests and see what explodes.” There are different types:
- Load Testing - can it handle the expected number of users?
- Stress Testing - at what point does it break?
- Spike Testing - how does it react to sudden bursts?
- Soak Testing - does it hold up over a long time?
- Scalability Testing - does throwing more resources at it actually help?
Simple enough on paper. The hard part is figuring out which one you need and what numbers are acceptable. I had no clue what “good response time” meant for my API. 200ms? 500ms? I just didn’t want it to crash.
Why k6
Looked at a few tools, JMeter, Gatling, Locust, Artillery. Went with k6 because:
- Written in Go (fitting, since my API is too)
- Tests are just JavaScript
- CLI output looks clean
- Grafana Labs maintains it so the docs are solid and it’s not going anywhere
Installing on my server was straightforward:
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6First test
k6 tests are JavaScript files with a default export function. Each virtual user (VU) runs that function. Here’s the first thing I wrote against my API:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10,
duration: '30s',
thresholds: {
http_req_duration: ['p(95)<500'],
},
};
export default function () {
const res = http.get('https://api.patwos.dev/api/v1/votes/1');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}Ten virtual users, thirty seconds, 95th percentile under 500ms. Ran it and watched the terminal do its thing.
Results
Good news: held up fine at 10 VUs. Bad news: that’s not a lot.
At 50 VUs response times started climbing. At 100, some requests timed out. Turns out the rate limiter I configured (100 req/s) was doing its job, which is nice but also meant I was mostly testing the rate limiter and not the API itself.
Disable or bump up your rate limiter when you’re performance testing. Unless the rate limiter is what you’re testing.
Stages
Things got more interesting with stages. Instead of a flat number of users you can simulate gradual ramp-ups:
export const options = {
stages: [
{ duration: '1m', target: 20 }, // ramp up
{ duration: '3m', target: 20 }, // hold
{ duration: '1m', target: 50 }, // push it
{ duration: '2m', target: 50 }, // hold at peak
{ duration: '1m', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(95)<400', 'p(99)<800'],
http_req_failed: ['rate<0.01'],
},
};Way more realistic than throwing a fixed number of users at it and hoping for the best.
Checks vs thresholds
Two things I wish I understood sooner:
- Checks are like assertions. They tell you if responses match your criteria but they won’t fail the test.
- Thresholds are the actual pass/fail. If breached, k6 exits with a non-zero code. Good for CI/CD.
So you might see that 2% of responses were slow (check), but that doesn’t kill the run. If 10% are failing though, that should be a hard stop (threshold).
What’s next
I’ve barely scratched the surface. k6 supports custom metrics, different executor types, browser testing, and what I actually want to try next: hooking it up to Grafana and Prometheus.
Right now I’m just reading CLI output which is fine for quick checks but useless for spotting trends over time.
Final Thoughts
Performance testing felt like one of those things that’s harder to start than to actually do. k6 is approachable if you know JS, and the docs are well put together. The tricky bit isn’t the tool, it’s knowing what to ask about your system.
Now I need to figure out why my API chokes at 100 users.