Skip to main content
May 29, 2026test environment variables k6

By Performate

Managing Environments and Variables in k6 for Safer Tests

Use __ENV, CLI flags, and modular options to separate dev/stage/prod configs without leaking secrets—aligned with k6's runtime model.

Mixing staging URLs with production tokens has caused real outages—and duplicated k6 scripts that silently diverge within a sprint. k6 treats configuration as data supplied at runtime: environment variables via __ENV, CLI -e flags, and options blocks parsed at startup (environment variables, options).

Managing environments in k6 is not about .env files checked into git. It is about one script, explicit mandatory variables, and profiles that CI, Docker, and local shells inject consistently. In this guide you will learn patterns for endpoints and secrets, env-driven scenario tuning, and anti-patterns that leak credentials or hide wrong-target runs.

Pair this with Postman to k6 when collections already encode named environments, and load testing in CI/CD for injecting secrets in pipelines.

Why environment drift breaks load tests

The same script against the wrong base URL does not fail loudly—it produces plausible latency numbers for the wrong system.

  • Hardcoded prod URLs "temporarily" become permanent; staging never gets exercised.
  • Forked scripts per env (checkout-staging.js, checkout-prod.js) drift on thresholds and payloads.
  • Missing validation lets runs proceed halfway through a soak before undefined tokens surface.
  • Container vs laptop drift: TZ, DNS, and OAuth clock skew break auth refresh scenarios identically in CI and nowhere locally.
  • Logged secrets in debug output end up in CI artifacts forever.

Think of configuration like airport gates: the flight plan (script) stays fixed; boarding passes (env vars) decide which runway you actually hit.

When tests "pass" against the wrong target

Green thresholds against an idle dev sandbox prove nothing about staging readiness. Mandatory env validation in setup() turns misconfiguration into an immediate, readable failure before virtual users spawn.

Practical k6 implementation: profiles without script forks

Keep scenario definitions stable; vary duration, rates, and thresholds through env vars parsed into options. Document required keys at the top of every script.

Example script (illustrative—not a production-ready test). Uses fictional URLs, tokens, and tuning values. Adapt variable names and profiles to your org.

What this example demonstrates:

  • Mandatory env guard: setup() throws when BASE_URL or API_TOKEN is missing.
  • Profile switch: ENV_PROFILE=dev|stage|load selects duration and rate without separate files.
  • Shared route logic: one hitCheckout() function; env only changes targets and load shape.
  • Safe defaults: staging URL as fallback only for local dev—not for CI (CI must pass explicit -e).
import http from 'k6/http';
import { check, sleep } from 'k6';

// REQUIRED: BASE_URL, API_TOKEN — pass via -e or CI secrets
const BASE = __ENV.BASE_URL || 'https://staging.example.com';
const TOKEN = __ENV.API_TOKEN;
const PROFILE = __ENV.ENV_PROFILE || 'stage'; // dev | stage | load

const profiles = {
  dev: { duration: '30s', rate: 2, maxVUs: 5 },
  stage: { duration: '2m', rate: 10, maxVUs: 40 },
  load: { duration: '10m', rate: 40, maxVUs: 200 },
};

const cfg = profiles[PROFILE] || profiles.stage;

export function setup() {
  if (!__ENV.BASE_URL && PROFILE !== 'dev') {
    throw new Error('Set BASE_URL explicitly for non-dev profiles');
  }
  if (!TOKEN) {
    throw new Error('Set API_TOKEN via -e or CI secret mapping');
  }
  return { base: BASE };
}

export const options = {
  scenarios: {
    checkout: {
      executor: 'constant-arrival-rate',
      rate: cfg.rate,
      timeUnit: '1s',
      duration: cfg.duration,
      preAllocatedVUs: Math.min(cfg.maxVUs, 20),
      maxVUs: cfg.maxVUs,
      exec: 'hitCheckout',
      tags: { env: PROFILE, route: 'checkout' },
    },
  },
  thresholds: {
    http_req_failed: ['rate<0.01'],
    'http_req_duration{route:checkout}': ['p(95)<900'],
  },
};

export function hitCheckout() {
  const res = http.post(
    `${BASE}/checkout`,
    JSON.stringify({ sku: 'SKU-100', qty: 1 }),
    {
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${TOKEN}`,
      },
      tags: { route: 'checkout', env: PROFILE },
    },
  );
  check(res, { 'checkout 2xx': (r) => r.status >= 200 && r.status < 300 });
  sleep(0.3);
}

Patterns that work

  • __ENV for secrets and endpoints—read in setup() or module scope with loud failures (environment variables).
  • CLI injection: k6 run -e BASE_URL=... -e API_TOKEN="$TOKEN" script.js—never commit secrets.
  • Explicit stages per environment via Number(__ENV.VUS) or profile maps—avoid duplicated options blocks.
  • Docker and cloud runners: map secrets to env vars identically; verify TZ and DNS match laptops.
  • Shared defaults + overrides: document BASE_URL, TENANT_ID, ENV_PROFILE in a header comment block.

Anti-patterns to avoid

  • Hardcoding prod URLs—even "for one demo."
  • Duplicating entire scripts per env instead of parameters.
  • Logging tokens during debugging—scrub CI artifacts and local consoles.
  • Silent fallbacks to staging in CI jobs meant to hit preview environments.

Pro tip (example commands): same script, three targets—no copy/paste forks.

# Local dev — short, low rate
k6 run checkout.js -e ENV_PROFILE=dev -e API_TOKEN="$DEV_TOKEN"

# CI staging gate
k6 run checkout.js -e ENV_PROFILE=stage \
  -e BASE_URL="$STAGING_URL" -e API_TOKEN="$STAGING_TOKEN"

# Scheduled load profile
k6 run checkout.js -e ENV_PROFILE=load \
  -e BASE_URL="$STAGING_URL" -e API_TOKEN="$STAGING_TOKEN"

What these commands demonstrate: profile selection is a flag change; BASE_URL must be explicit for CI and scheduled runs.

Decision framework: when to use which pattern

SituationRecommended action
Local iterationENV_PROFILE=dev; optional staging default URL
CI smoke gateMandatory BASE_URL + secrets; short profile only
Scheduled soakENV_PROFILE=load; explicit staging or isolated env
Multi-tenant APIsTENANT_ID env; tag requests {tenant:...}
Postman-sourced testsMap collection environments to -e flags at export time
OAuth clock sensitivityAlign TZ; validate token refresh in setup()

Use profile maps if duration and rate change per environment but routes stay identical.

Use mandatory setup() checks if wrong-target runs are unacceptable—typical for any shared script repo.

Use collection environment export if QA already maintains Postman env JSON—see Postman to k6.

Observability and safety checklist

Before sharing scripts across teams:

  • List required env vars in script header and README fragment in PR template.
  • Fail fast in setup() when secrets or BASE_URL missing for non-dev profiles.
  • Tag requests with env: profile for filtered reporting.
  • Scrub logs; disable verbose HTTP debug in CI.
  • Archive env profile name (not secret values) alongside summary JSON per run.

How Performate simplifies environment switching

Centralizing imports and toggles beats maintaining three copies of the same logic. Below is a concrete workflow example for the checkout API this article uses.

Example: staging vs load profile without script forks

  1. Import one Postman collection with environment variables for baseUrl and apiToken. Problem solved: QA environments become performance environments without re-entry.
  2. Define environment profiles in Performate—Dev, Staging, Load—with duration and arrival rate presets matching ENV_PROFILE above. Problem solved: engineers toggle targets in UI instead of editing executor blocks.
  3. Map collection variables to scenario fields once; re-use across smoke and soak scenarios. Problem solved: payload and auth headers stay synchronized.
  4. Run Staging profile for pre-release gates; switch to Load profile for scheduled regression—same requests, different shape. Problem solved: no checkout-staging.js / checkout-load.js drift.
  5. Export k6 script with __ENV placeholders for CI secret injection. Problem solved: desktop tuning and pipeline execution share one artifact.
  6. Share reports filtered by environment tag so incidents compare the same profile baseline. Problem solved: on-call reads env:stage, not mystery laptop runs.

That workflow maps directly to the cta in this post: manage environments and variable switches without duplicating scripts.

Closing takeaway

Safer k6 tests treat environment as runtime input: one script, explicit mandatory variables, profiles for shape—not separate repos that diverge.

Add a setup() guard for BASE_URL and API_TOKEN this week, delete the "temporary" prod URL in your oldest script, and document which profile CI runs by default.

Try Performate free | Book a demo | k6 options reference

Ready to optimize your API performance?

Use Performate to manage environments and variable switches without duplicating scripts.

← Back to all posts