Skip to main content
Create custom k6 extensions to add JavaScript modules, output formats, secret sources, or CLI subcommands. Extensions are written in Go and compiled into k6 using xk6.

Prerequisites

  • Go 1.22 or later
  • Understanding of Go programming
  • Familiarity with k6 concepts
  • Git for version control

Extension Types

You can create four types of extensions:
  1. JavaScript Extensions: Add custom JavaScript APIs
  2. Output Extensions: Create custom result outputs
  3. Secret Source Extensions: Implement secret providers
  4. Subcommand Extensions: Add CLI subcommands
See Extensions Overview for details on each type.

Project Setup

1
Create Repository
2
Create a new Go module for your extension:
3
mkdir xk6-myextension
cd xk6-myextension
go mod init github.com/yourname/xk6-myextension
4
Add k6 Dependency
5
go get go.k6.io/k6@latest
6
Create Main File
7
Create your extension implementation:
8
touch myextension.go
Follow the xk6- naming convention for discoverability. Name your extension repository xk6-extensionname.

Creating a JavaScript Extension

JavaScript extensions add custom modules that can be imported in test scripts.

Basic Structure

package myextension

import (
    "go.k6.io/k6/js/modules"
)

func init() {
    modules.Register("k6/x/myextension", New())
}

type RootModule struct{}
type ModuleInstance struct {
    vu modules.VU
}

func New() *RootModule {
    return &RootModule{}
}

func (r *RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
    return &ModuleInstance{vu: vu}
}

func (mi *ModuleInstance) Exports() modules.Exports {
    return modules.Exports{Default: mi}
}

Adding Functionality

Add methods that will be available in JavaScript:
func (mi *ModuleInstance) Greet(name string) string {
    return "Hello, " + name + "!"
}

func (mi *ModuleInstance) Add(a, b int) int {
    return a + b
}
Use in JavaScript:
import myext from 'k6/x/myextension';

export default function () {
  console.log(myext.greet('World'));
  console.log(myext.add(2, 3));
}

Working with Metrics

Create custom metrics in your extension:
import (
    "go.k6.io/k6/js/modules"
    "go.k6.io/k6/metrics"
    "time"
)

type ModuleInstance struct {
    vu     modules.VU
    custom *metrics.Metric
}

func (r *RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
    return &ModuleInstance{
        vu:     vu,
        custom: vu.InitEnv().Registry.MustNewMetric("custom_metric", metrics.Counter),
    }
}

func (mi *ModuleInstance) RecordValue(value float64) {
    state := mi.vu.State()
    ctx := mi.vu.Context()
    
    tags := state.Tags.GetCurrentValues().Tags
    metrics.PushIfNotDone(ctx, state.Samples, metrics.Sample{
        Time:       time.Now(),
        TimeSeries: metrics.TimeSeries{Metric: mi.custom, Tags: tags},
        Value:      value,
    })
}
Always check if vu.State() is nil. It will be nil in the init context where metric emission is not allowed.

Real Example

Here’s a real JavaScript extension from k6’s test suite (xk6-js-test/jstest.go:1):
package jstest

import (
    "fmt"
    "time"
    "go.k6.io/k6/js/modules"
    "go.k6.io/k6/metrics"
)

func init() {
    modules.Register("k6/x/jsexttest", New())
}

type (
    RootModule struct{}
    JSTest struct {
        vu   modules.VU
        foos *metrics.Metric
    }
)

func New() *RootModule {
    return &RootModule{}
}

func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
    return &JSTest{
        vu:   vu,
        foos: vu.InitEnv().Registry.MustNewMetric("foos", metrics.Counter),
    }
}

func (j *JSTest) Exports() modules.Exports {
    return modules.Exports{Default: j}
}

func (j *JSTest) Foo(arg float64) (bool, error) {
    state := j.vu.State()
    if state == nil {
        return false, fmt.Errorf("the VU State is not available in the init context")
    }
    
    ctx := j.vu.Context()
    tags := state.Tags.GetCurrentValues().Tags.With("foo", "bar")
    metrics.PushIfNotDone(ctx, state.Samples, metrics.Sample{
        Time:       time.Now(),
        TimeSeries: metrics.TimeSeries{Metric: j.foos, Tags: tags},
        Value:      arg,
    })
    
    return true, nil
}

Creating an Output Extension

Output extensions send test results to custom backends.

Basic Implementation

See Creating Custom Outputs for a comprehensive guide. Quick example:
package myoutput

import (
    "go.k6.io/k6/output"
    "go.k6.io/k6/metrics"
)

func init() {
    output.RegisterExtension("myoutput", New)
}

type Output struct {
    output.SampleBuffer
    params output.Params
}

func New(params output.Params) (output.Output, error) {
    return &Output{params: params}, nil
}

func (o *Output) Description() string {
    return "my-custom-output"
}

func (o *Output) Start() error {
    return nil
}

func (o *Output) Stop() error {
    samples := o.GetBufferedSamples()
    // Process samples
    return nil
}

Creating a Secret Source Extension

Secret source extensions provide secure configuration values.

Implementation

package mysecrets

import (
    "go.k6.io/k6/ext"
)

func init() {
    ext.Register("mysecrets", ext.SecretSourceExtension, &SecretSource{})
}

type SecretSource struct{}

func (s *SecretSource) GetSecret(key string) (string, error) {
    // Fetch secret from your backend
    return fetchSecretFromVault(key)
}

Creating a Subcommand Extension

Subcommand extensions add new CLI commands to k6.

Implementation

package mycommand

import (
    "github.com/spf13/cobra"
    "go.k6.io/k6/ext"
)

func init() {
    ext.Register("mycommand", ext.SubcommandExtension, NewCommand)
}

func NewCommand() *cobra.Command {
    return &cobra.Command{
        Use:   "mycommand",
        Short: "My custom command",
        RunE: func(cmd *cobra.Command, args []string) error {
            // Command implementation
            return nil
        },
    }
}
Use it:
k6 mycommand [args]

Extension Registration

All extensions must register in an init() function using ext.Register() (ext/ext.go:62):
import "go.k6.io/k6/ext"

func init() {
    ext.Register(
        "extensionname",           // Name
        ext.JSExtension,            // Type
        New(),                      // Module/implementation
    )
}
Registration happens automatically when the extension package is imported during build.

Testing Your Extension

Unit Tests

package myextension

import (
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestGreet(t *testing.T) {
    mi := &ModuleInstance{}
    result := mi.Greet("World")
    assert.Equal(t, "Hello, World!", result)
}

Integration Tests

Create a test script:
// test.js
import myext from 'k6/x/myextension';
import { check } from 'k6';

export default function () {
  const result = myext.greet('k6');
  check(result, {
    'greeting works': (r) => r === 'Hello, k6!',
  });
}
Build and test:
xk6 build --with github.com/yourname/xk6-myextension=.
./k6 run test.js

Building and Using

Local Development

Build k6 with your local extension:
xk6 build --with github.com/yourname/xk6-myextension=.

Publishing

1
Push to GitHub
2
git add .
git commit -m "Initial extension implementation"
git tag v0.1.0
git push origin main --tags
3
Use Published Extension
4
Others can now use:
5
xk6 build --with github.com/yourname/[email protected]

Best Practices

1
Follow Go Conventions
2
Use standard Go project layout and naming conventions.
3
Document Your API
4
Provide clear documentation for all exported functions:
5
// Greet returns a greeting message for the given name.
func (mi *ModuleInstance) Greet(name string) string {
    return "Hello, " + name + "!"
}
6
Handle Errors Properly
7
Return errors to JavaScript instead of panicking:
8
func (mi *ModuleInstance) DoSomething() error {
    if err := operation(); err != nil {
        return fmt.Errorf("operation failed: %w", err)
    }
    return nil
}
9
Validate Inputs
10
Check all inputs from JavaScript:
11
func (mi *ModuleInstance) SetValue(val int) error {
    if val < 0 {
        return errors.New("value must be non-negative")
    }
    // ...
}
12
Use Context Appropriately
13
Respect cancellation:
14
func (mi *ModuleInstance) LongOperation() error {
    ctx := mi.vu.Context()
    select {
    case <-ctx.Done():
        return ctx.Err()
    case result := <-doWork():
        return nil
    }
}
15
Avoid Blocking
16
Don’t block VU execution with long operations:
17
// Bad: blocks VU
func (mi *ModuleInstance) SlowSync() {
    time.Sleep(5 * time.Second)
}

// Good: async or fast
func (mi *ModuleInstance) FastAsync() chan Result {
    ch := make(chan Result, 1)
    go func() {
        // Long operation
        ch <- result
    }()
    return ch
}

Common Patterns

Connection Pooling

type ModuleInstance struct {
    vu   modules.VU
    pool *ConnectionPool
}

func (r *RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
    return &ModuleInstance{
        vu:   vu,
        pool: getOrCreatePool(vu),
    }
}

Configuration

type Config struct {
    Host string `json:"host"`
    Port int    `json:"port"`
}

func (r *RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
    var config Config
    if err := json.Unmarshal(vu.InitEnv().Options, &config); err != nil {
        // Handle error
    }
    return &ModuleInstance{config: config}
}

Resource Cleanup

func (mi *ModuleInstance) Close() error {
    if mi.connection != nil {
        return mi.connection.Close()
    }
    return nil
}

Debugging

Enable Verbose Logging

import "github.com/sirupsen/logrus"

func (mi *ModuleInstance) Debug(msg string) {
    logger := mi.vu.InitEnv().Logger
    logger.WithField("extension", "myext").Debug(msg)
}

Use k6 Verbose Mode

./k6 run --verbose test.js

Example Extensions

Study these official examples:

Resources

Next Steps

Build docs developers (and LLMs) love