Documentation
Getting Started
kFunk CLI is open source and available on GitHub. Install it globally:
# Using Homebrew
brew install kfunk
# Or using npm
npm install -g kfunkWriting a Test Script
A kFunk test script is a JavaScript file that exports an object with lifecycle hooks. You can use any npm packages — just install them in your project directory.
// my-test.js
export default {
// Called once per virtual user before the test loop starts
async setup(ctx) {
ctx.state.url = "https://api.example.com/health"
},
// Called repeatedly for the test duration
async run(ctx, metrics) {
await metrics.measure("http_get", async () => {
const res = await fetch(ctx.state.url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
await res.json()
})
metrics.increment("requests")
},
// Called once per virtual user after the test loop ends
async teardown(ctx) {
// clean up resources
},
}Lifecycle Hooks
| Hook | Required | Description |
|---|---|---|
setup(ctx) | No | Initialize state, open connections, prepare data |
run(ctx, metrics) | Yes | The main test loop body — called repeatedly for the duration |
teardown(ctx) | No | Clean up resources (close connections, etc.) |
Context Object (ctx)
| Property | Type | Description |
|---|---|---|
ctx.vuId | number | Virtual user ID (0-based) |
ctx.state | object | Persistent state bag for the VU — store connections, counters, etc. |
Metrics API
The metrics object passed to run() provides:
| Method | Description |
|---|---|
metrics.measure(name, fn) | Time an async function and record the duration under name |
metrics.startTimer() | Start a manual timer — returns a timestamp |
metrics.record(name, startTime) | Record duration since startTime under name |
metrics.increment(name, value?) | Increment a counter by value (default 1) |
Running a Test
kfunk run my-test.js --vus 10 --duration 30CLI Options
| Option | Description | Default |
|---|---|---|
--vus <n> | Number of virtual users (concurrent) | 10 |
--duration <s> | Test duration in seconds | 30 |
--stagger <ms> | Delay between spawning each VU (ms) | 0 |
--service-url <url> | Worker URL (enables distributed mode without an infra plugin) | — |
--workers <n> | Number of workers (distributed only) | 1 |
--worker-stagger <ms> | Delay between launching each worker (ms) | 0 |
--config <path> | Path to kfunk config file | auto-detected |
--json | Output raw JSON instead of formatted table | false |
Example: HTTP Load Test
// example-test.js
export default {
async setup(ctx) {
ctx.state.url = "https://jsonplaceholder.typicode.com/posts/1"
},
async run(ctx, metrics) {
await metrics.measure("http_get", async () => {
const res = await fetch(ctx.state.url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
await res.json()
})
metrics.increment("requests")
},
}kfunk run example-test.js --vus 5 --duration 10Example: NoLag Broker Test
Test a NoLag real-time broker with publish/subscribe roundtrip metrics.
// example-nolag.js
import { NoLag } from "@nolag/js-sdk"
const TOKEN = process.env.NOLAG_TOKEN
const TOPIC = process.env.NOLAG_TOPIC || "kfunk/load-test/messages"
export default {
async setup(ctx) {
if (!TOKEN) throw new Error("NOLAG_TOKEN env var is required")
const client = NoLag(TOKEN, { qos: 1, reconnect: false })
await client.connect()
ctx.state.client = client
ctx.state.seq = 0
ctx.state.pending = new Map()
client.subscribe(TOPIC)
client.on(TOPIC, (data) => {
if (data._vuId === ctx.vuId) {
const sent = ctx.state.pending.get(data._seq)
if (sent) {
ctx.state.lastRoundtrip = performance.now() - sent
ctx.state.pending.delete(data._seq)
}
}
})
},
async run(ctx, metrics) {
const seq = ctx.state.seq++
ctx.state.pending.set(seq, performance.now())
await metrics.measure("publish", async () => {
ctx.state.client.emit(TOPIC, {
_vuId: ctx.vuId, _seq: seq, text: "kfunk"
}, { echo: true })
})
metrics.increment("messages_sent")
await new Promise(r => setTimeout(r, 100))
if (ctx.state.lastRoundtrip !== undefined) {
metrics.record("roundtrip", ctx.state.lastRoundtrip)
metrics.increment("messages_received")
ctx.state.lastRoundtrip = undefined
}
},
async teardown(ctx) {
ctx.state.client?.disconnect()
},
}NOLAG_TOKEN=your_token kfunk run example-nolag.js --vus 10 --duration 30Plugin System
kFunk supports plugins to extend its functionality. There are two plugin types: event plugins stream test lifecycle events in real-time (from both the CLI and distributed workers), and infrastructure plugins manage distributed workers on any cloud provider.
Configuration
Create a kfunk.config.js file in your project root:
// kfunk.config.js
export default {
plugins: [
"@kfunk-load/plugin-gcp",
{ name: "@kfunk-load/plugin-nolag", config: { apiKey: process.env.NOLAG_KEY } },
],
infra: "@kfunk-load/plugin-gcp",
"@kfunk-load/plugin-gcp": {
region: "us-central1",
serviceName: "kfunk-worker",
},
}plugins lists plugin packages (or { name, config } objects for inline config). infra selects which plugin to use for infrastructure. Top-level keys matching a plugin name provide plugin-specific configuration. No config file is required — without one, kFunk runs tests locally with no plugins.
Event Plugins
Event plugins observe the test lifecycle. Their hooks fire on the CLI and inside distributed workers:
| Hook | Where | Description |
|---|---|---|
onTestStart(context) | CLI | Test is starting |
onVUStart(vuId, totalVUs) | CLI + Workers | A VU is about to start |
onVUComplete(vuMetrics) | CLI + Workers | A VU finished with its metrics |
onProgress(message) | CLI | Progress update |
onTestEnd(context, results) | CLI | Test complete with aggregated results |
In distributed mode, event plugin configs are serialized and sent to workers. Workers load the event plugins locally and fire onVUStart/onVUComplete hooks in real-time during VU execution. The infra plugin's deploy() automatically includes event plugin npm packages in the worker image.
Infrastructure Plugins
Infrastructure plugins manage distributed workers on any cloud:
| Method | Description |
|---|---|
deploy(options) | Deploy workers (builds image with event plugin deps), returns service URL |
runDistributed(options) | Run a distributed test, returns results |
getAuthHeaders(serviceUrl) | Get auth headers for worker requests |
teardown(options) | Destroy deployed workers |
Available Plugins
| Package | Type | Description |
|---|---|---|
@kfunk-load/plugin-gcp | Infrastructure | Deploy and run distributed tests on Google Cloud Run |
@kfunk-load/plugin-nolag | Event | Stream test events to a NoLag real-time broker |
Writing a Plugin
A plugin package exports a factory function as its default export:
// Event plugin example
export default function(config) {
return {
name: "my-event-plugin",
type: "event",
async onTestStart(context) { /* ... */ },
async onVUComplete(vuMetrics) { /* ... */ },
async onTestEnd(context, results) { /* ... */ },
async destroy() { /* cleanup */ },
}
}Distributed Testing
For large-scale tests, kFunk can distribute your test across multiple workers. With an infrastructure plugin configured, deployment and execution are seamless:
# Deploy workers (infra plugin builds image and deploys)
kfunk deploy
# Run distributed — no --service-url needed
kfunk run my-test.js --workers 10 --vus 20 --duration 60The infra plugin handles authentication, image building (including event plugin dependencies), and worker orchestration. Results are aggregated on the CLI after all workers complete.
GCP Cloud Run (via plugin)
Install the GCP plugin and configure it:
npm install @kfunk-load/plugin-gcp// kfunk.config.js
export default {
plugins: ["@kfunk-load/plugin-gcp"],
infra: "@kfunk-load/plugin-gcp",
"@kfunk-load/plugin-gcp": {
region: "us-central1",
serviceName: "kfunk-worker",
},
}Prerequisites (GCP)
You need the Google Cloud CLI installed and authenticated.
# Install gcloud CLI
brew install google-cloud-sdk
# Authenticate
gcloud auth login
# Set your GCP project
gcloud config set project YOUR_PROJECT_ID
# Enable required APIs
gcloud services enable run.googleapis.com \
artifactregistry.googleapis.com \
cloudbuild.googleapis.comDeploy & Run
# Deploy worker to Cloud Run
kfunk deploy
# Run a distributed test
kfunk run my-test.js --workers 10 --duration 30Legacy Mode (without plugins)
You can also use --service-url directly without an infra plugin. This uses the built-in orchestrator — workers must be publicly accessible or you must handle auth separately.
kfunk run my-test.js \
--service-url https://kfunk-worker-xxxxx.run.app \
--workers 10 \
--duration 30Output
kFunk outputs a summary table with latency percentiles (p50, p90, p95, p99), min/max/avg, throughput, and counter totals. Use --json to get machine-readable output for CI/CD pipelines.