Picture this: you’ve set up your new k6 performance tests, eager to see how your APIs hold up under load. The scripts fire up, requests start flowing, everything looks smooth… until suddenly, requests begin to fail. Not because your API broke, but because your authentication token quietly expired mid-test.
It’s one of those frustrating moments where your load test results are ruined by something that has nothing to do with the performance of the application being tested. If your tokens expire every few minutes, but your tests run for a longer duration, you’re guaranteed to hit this wall.
In this blog, I’ll show you how we combined Node.js, Puppeteer, and Express to automatically refresh tokens in the background and make sure our k6 tests always run with valid authentication.
Problem Statement
Our challenge:
API tests take between 1–15 minutes.
The auth token expires after 5 minutes.
K6 runs in its own Go-based JavaScript engine (Goja), which means Node.js modules (like fs) don’t work.
If we ignore this issue, our performance test results become unreliable because tests fail due to expired tokens instead of system performance.
Solution: Step-by-Step Process
Here’s the approach we took to keep tokens fresh during long-running K6 tests.
Step 1: Scheduled Token Refresh Using Node.js
We already had a login method using Puppeteer to fetch tokens. We extended it to save tokens in a file and refresh them every 4.5 minutes using setInterval( duration should be close to token expiry).
import fs from 'fs';
const tokenFilePath = './token.json';
export async function tokenRefresh() {
// Assume your login function returns the auth token
const authorization = await login();
fs.writeFileSync(tokenFilePath, JSON.stringify({
access_token: authorization,
expires_at: new Date(Date.now() + 4.5 * 60 * 1000).toISOString()
}, null, 2));
}
setInterval(async () => {
await tokenRefresh();
}, 4.5 * 60 * 1000);
At this point, we have a token.json file that always contains a fresh token. But how do we access this from K6?
Step 2 : Setting Up a Token Server with Express
Since K6 does not support Node.js modules like fs, we couldn’t directly read the token from token.json during test execution. To address this, we introduced a lightweight Express server that exposes the token via an API endpoint, making it accessible to K6 whenever needed.
Additionally, we embedded the token refresh logic (setInterval) within the server itself. This way, the server automatically refreshes the token at regular intervals, eliminating the need to keep a separate login script running in parallel.
// token-server.ts
import express from 'express';
import fs from 'fs';
const app = express();
const port= 4000;
app.get('/token', (req, res) => {
try {
const data = JSON.parse(fs.readFileSync('./token.json', 'utf8'));
res.json({ access_token: data.access_token });
} catch (err) {
res.status(500).json({ error: 'Failed to read token' });
}
});
app.listen(port, () => {
await refreshToken();
setInterval(async () => {
try {
console.log(' Auto-refreshing token...');
await refreshToken();
} catch (err) {
console.error('Token refresh failed:', err);
}
}, 4.5 * 60 * 1000);
});
This server:
Reads the token from the file.
Exposes it at http://localhost:4000/token.
Keeps refreshing it in the background.
Note: You can either keep the login and refresh functions in one file and the server code in another, or combine them into a single file that handles login, token refresh, and serving the token. Combining everything into one file can simplify setup and reduce the overhead of managing multiple files.
Step 3: Fetching Fresh Tokens Inside k6
We created a separate helper script (fetchFreshToken.js) that k6 can use to fetch tokens from our local server. This avoids direct file reading (which k6 doesn’t support).
// fetchFreshToken.js
import http from 'k6/http';
let authToken = '';
let lastTokenReadTime = 0;
const tokenRefreshInterval = 4 * 60 * 1000;
export function getAuthToken() {
const now = Date.now();
if (!authToken || (now - lastTokenReadTime > tokenRefreshInterval)) {
const res = http.get('http://localhost:4000/token');
authToken = JSON.parse(res.body).access_token;
lastTokenReadTime = now;
}
return authToken;
}
export function getParams() {
return {
headers: { Authorization: `Bearer ${getAuthToken()}` }
};
}
Notice the separation:
getAuthToken() talks to the local token server and caches the token.
getParams() updates request headers with the latest token.
Then, in our actual k6 test scripts, it’s super simple. We are simply calling getParams().
import http from 'k6/http';
import { getParams } from './fetchFreshToken.js';
export default function () {
const res = http.get(`${__ENV.BASE_URL}/api/data`, getParams());
console.log(`Response: ${res.status}`);
}
Behind the scenes, getParams() makes sure the header always contains a valid token.
Step 4: Running k6 & Token Server in Parallel
At first, we ran the token server and K6 script in separate terminals. But that was messy. To fix this, we automated both into a single command inside package.json:
"scripts": {
"run:k6": "cross-env TOKEN_SCRIPT=$TOKEN_SCRIPT K6_SCRIPT=$K6_SCRIPT bash -c 'ts-node $TOKEN_SCRIPT & PID=$!; trap \"kill $PID 2>/dev/null\" EXIT INT TERM; sleep 20; k6 run --env $K6_SCRIPT; kill -0 $PID 2>/dev/null && kill $PID 2>/dev/null; wait $PID 2>/dev/null'"
}
Run it like this:
TOKEN_SCRIPT=<path to login file> K6_SCRIPT=<path to k6 script> npm run run:k6
This starts the token server, waits a bit, runs the K6 test, and cleans up afterward.
Key Takeaways
K6 doesn’t support Node.js modules, so you can’t directly refresh tokens inside K6.
A separate Node.js token server is a reliable workaround.
By fetching tokens via HTTP inside K6, you ensure your tests never fail due to expired tokens.
Running everything in parallel with an npm script makes the workflow seamless.
Final Thoughts
Token expiry during performance tests is a common but solvable challenge. By combining Puppeteer, Express, and some scripting, we built a reliable pipeline that keeps k6 tests running without interruptions from expired tokens.
If you’re running performance tests on APIs with short-lived tokens, try this approach — it’ll save you a lot of headaches.