Learn k6 - Testing Security & Best Practices
Series/Learn k6/Episode 12
Episode 12 of 19

Learn k6 - Testing Security & Best Practices

Staying secure while load testing: respecting rate limits with deliberate throttling, refusing header spoofing, injecting secrets via environment variables instead of writing them in the script, and isolating test environments so production applications aren't damaged by tests.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

In episode 11 you now hold tokens, sessions, and API keys inside your script. That capability comes with new responsibility: a load test is fundamentally very dense real traffic. Executed wrongly, a test meant to measure performance can trigger security alarms, flood rate limiters, get the office IP blocked, or even corrupt production environment data.

Imagine a fire drill in a building that's actually occupied. You need a realistic drill, but you can't put out a fake fire with water that leaves everyone soaked. This episode is about running that drill safely: how to test without breaking the application's security rules, how to protect the credentials you use, and how to make sure production environments don't become the victim.

Main Discussion

A Load Test Is Real Traffic: Respect Security Rules

Every request k6 sends is a real request. In the eyes of the application, WAF, and rate limiter, there is no difference between a k6 request and a user request. The first thing you must do before running a test: communicate with the environment owners. When is the test window? What is the agreed maximum load? Are there alarms that can be temporarily muted? A well-prepared load test is a collaborative effort with the infra team, not a solo act.

Throttling: Control the Rate, Don't Attack

One of the biggest beginner mistakes is launching thousands of VUs all at once at the same endpoint. That's not realistic load — it resembles a DDoS attack, and the WAF will predictably treat it as one. The solution is deliberate ramp-up:

options — ramp the load up gradually
export const options = {
    stages: [
        { duration: "1m", target: 10 },
        { duration: "3m", target: 50 },
        { duration: "3m", target: 100 },
        { duration: "2m", target: 100 },
        { duration: "1m", target: 0 },
    ],
    thresholds: {
        http_req_failed: ["rate<0.01"],
    },
};

These stages raise the load from 10 to 100 VUs gradually, hold it, then bring it down. There are three reasons this isn't just politeness but correct technique:

  • Realistic. Users don't appear all at once; they arrive gradually. Ramp-up produces a load curve that can be compared with production traffic.
  • Measurable. You can observe at what point latency starts to degrade, because the increase is monitored, not an explosion.
  • Safe. Rate limiting and auto-scaling systems get time to react naturally, rather than being overwhelmed instantly.

Important

Before a test, ask the infra team about the rate limits and IP protections in your application. If your test load exceeds those limits, the result is a block, not performance data. Adjust the target VUs and ramp-up to the agreed limits — or schedule the test in an environment that doesn't have strict limits.

Header Spoofing and Rate Limit Protection: Don't Lie

There's a temptation you must resist: faking headers like X-Forwarded-For or randomly changing the User-Agent to trick the rate limiter into thinking there are many different clients. This is wrong in dangerous ways:

  • You're testing behavior that never happens in production — a real rate limiter wouldn't allow this pattern. The results are misleading.
  • The server can log your address and permanently block the office IP or CI machine.
  • To a WAF, spoofing patterns look more suspicious than normal k6 traffic.

If you genuinely need to simulate many clients from many IP addresses — for example to test a load balancer — k6 provides localIPs to use many real local IP addresses. And for global scale, load distribution is covered in upcoming episodes. The takeaway: test real behavior, with an honest identity.

Handling Secrets: __ENV, Not Hardcode

This is the most important rule of this episode, and breaking it means production credentials leaking into the repository. Putting passwords, tokens, or API keys inside script files is an invitation to the most embarrassing event in engineering: secrets committed to git.

A direct comparison:

auth.js — hardcode vs env injection
const TOKEN = "sk_live_9f8d7c6b5a4e3d2c1b0a"; 
const TOKEN = __ENV.TOKEN; 
 
const params = {
    headers: {
        Authorization: `Bearer ${TOKEN}`,
    },
};

The red line is a disaster waiting to happen. The green line is the correct way: the value comes from outside the script. k6 provides two complementary mechanisms:

  • k6 run -e TOKEN=... script.js — the -e flag injects the value into __ENV, which the script can read. This is the most common and explicit way.
  • --include-system-env-vars — forwards system environment variables into __ENV. Note: k6 run enables it by default, which means your entire shell environment (possibly containing many secrets) gets read. In CI, disable it with --include-system-env-vars=false so no environment secrets leak into the script or logs.

Warning

Two rules you must never break. First: never commit real credentials — once they enter git history, they're never truly gone; rotation is the only path forward. Second: never print secrets to logs — console.log(__ENV.TOKEN) in a script being debugged will display the token in the terminal and CI logs, which are usually easier to access than the repo itself.

The practical way to do it:

Inject secrets when running the test
k6 run -e TOKEN=sk_test_abc123 -e TEST_USER=budi script.js
In CI, restrict access to the system env
k6 run --include-system-env-vars=false -e TOKEN="$K6_TOKEN" script.js

In CI, secret values come from the pipeline's secret store (GitHub Actions secrets, GitLab CI variables), mapped to -e when running k6. Your script stays clean, and dangerous values never touch the repository.

Test Environment Isolation: Staging, Not Production

The healthiest habit in performance testing is confining yourself to an environment meant for testing: staging, or an environment that closely mirrors production. There are obvious reasons, plus ones that aren't always thought of:

  • Data. Test accounts use synthetic data, not real user accounts. A staging database can be re-populated without legal or ethical consequences.
  • Impact. The latency increases or errors during a test aren't felt by real users. The team can experiment freely.
  • Equivalence. An environment that closely mirrors production (similar specs, comparable configuration) gives trustworthy numbers — testing on the smallest possible machine only proves that the test can run.

If testing in a dedicated environment isn't possible (for example you want to measure production), constrain it with a time window, an agreed load, and isolated test accounts — then make sure everyone involved signs off on that plan. Also remember: __ENV.BASE_URL (the pattern from episodes 7 and 11) lets the same script be pointed at staging or production without changing a single line — use that, don't create two different scripts.

Security Testing Checklist

  • Credentials only via __ENV, filled at runtime through -e or the CI secret store. No secrets in the script.
  • --include-system-env-vars=false in CI so the system environment doesn't leak into the script.
  • No console.log or log.info that prints token values.
  • Load is increased gradually (stages), request rate within agreed limits.
  • No header spoofing to bypass the rate limiter.
  • Test target is staging or a test environment, with synthetic data.
  • The test plan (timing, load, environment) is communicated and approved by the system owners.

Conclusion

In this episode 12 you've made security part of the process: understanding that a load test is real traffic that must respect security rules, raising load gradually with stages so you don't attack, refusing header spoofing because the results are misleading and dangerous, injecting secrets via -e and __ENV instead of hardcoding, disabling --include-system-env-vars in CI, and isolating testing to the staging environment.

Key points to take away:

  • A test that doesn't respect rate limits is an attack, not a measurement.
  • Real credentials never go into scripts, commits, or logs.
  • -e KEY=value is the secret entry door; __ENV is the read door.
  • Test in an environment that closely mirrors production, with safe data.

Now your tests are safe and honest. The next question is: how do you read the results correctly? In episode 13 we'll cover Performance Analysis and Scripting Optimization — interpreting metrics like VU, latency, p95 and p99, trimming JavaScript overhead so the test engine doesn't lie, and structuring thresholds that target SLAs or SLOs. See you in episode 13!

Learn k6 - Testing Security & Best Practices | Learn k6