Claude
Skills
Sign in
Back

ring:load-testing-with-k6

Included with Lifetime
$97 forever

Load-testing with k6 for the LerianStudio/k6 Palantir platform: scaffolds product.yaml, smoke/load/stress/soak scenarios, a helper client, builds the webpack bundle, and verifies a local k6 run. Use when new API/gRPC endpoints or throughput-path changes need SLO validation under load, or a Palantir CI load gate is required. Skip when no network-facing endpoints are affected or changes are config-only or non-code.

Backend & APIs

What this skill does


# k6 Load Testing (Palantir Platform)

## When to use
- After integration testing passes
- Before production deploy of performance-sensitive changes
- New API endpoints or significant throughput-path changes
- Need to validate SLOs under load (latency, error rate, throughput)
- CI pipeline requires load test gate via Palantir

## Skip when
- Task is documentation-only, configuration-only, or non-code
- No HTTP/gRPC endpoints affected by the change
- Changes limited to static assets, configs, or non-runtime code
- Service has no network-facing interface

## Related
**Complementary:** ring:implementing-tasks, ring:reviewing-code


This skill generates k6 load tests following the Lerian k6 platform conventions.
Tests are structured for execution via Palantir (Self-Service Testing) and are
bundled by webpack into self-contained scripts deployed to EKS via k6-operator.

**Reference repository:** `LerianStudio/k6` — specifically `platform/` directory.

**Block conditions:**
- Test script missing `handleSummary` export = FAIL (Palantir can't collect results)
- `scenario.yaml` param names don't match `__ENV` vars in test.js = FAIL
- Test doesn't read VUS/DURATION from `__ENV` = FAIL
- No `checkResponse()` from shared utils = FAIL
- Missing `product.yaml` = FAIL

## Step 1: Validate Input

Required:
- `product` — product name in lowercase (e.g., `midaz`, `tracer`, `reporter`, `matcher`)
- `endpoints` — list of endpoints to test, each with method, path, and optional payload
- `base_port` — local dev port for the product (e.g., 3000 for midaz, 4020 for tracer)

Optional:
- `scenario_types` — which scenarios to generate (default: `[smoke, load, stress]`)
- `auth_type` — `bearer` (default, uses `shared/auth.js`) | `api-key` | `none`
- `api_key_header` — header name for API key auth (default: `X-API-Key`)
- `custom_thresholds` — override default thresholds
- `existing_product` — if true, extend existing product directory

## Step 2: Understand the Platform Structure

All test code lives under `platform/` in the `LerianStudio/k6` repo:

```
platform/
├── products/{product}/
│   ├── product.yaml              # Product metadata (read by Palantir)
│   ├── helpers/
│   │   └── client.js             # HTTP client for this product's API
│   └── scenarios/
│       └── {scenario}/
│           ├── scenario.yaml     # Catalog metadata (read by Palantir)
│           └── test.js           # k6 test script (webpack entry point)
├── shared/
│   ├── auth.js                   # getAuthHeaders(), authenticate()
│   ├── utils.js                  # checkResponse(), sleepWithJitter(), defaultHandleSummary()
│   └── palantir/                 # SDK for complex scenarios (fixtures, runtime)
│       ├── index.js              # scenario(), fixture(), createTestExports()
│       ├── runtime.js            # Builds k6 exports from config
│       ├── scenario.js           # Declarative scenario config builder
│       └── templates.js          # Built-in test type templates (smoke/quick/full/breakpoint/soak)
├── dist/                         # Webpack output (git-ignored)
├── build.js                      # Bundler entry point
├── webpack.config.js             # Auto-discovers products/*/scenarios/*/test.js
├── config.yaml                   # Platform-level test catalog metadata
└── package.json
```

### Two Patterns for Writing Tests

**Pattern A: Simple client (recommended for most tests)**

Product `helpers/client.js` provides `get()`, `post()`, `patch()`, `del()` scoped to
the product's base URL. Scenarios import the client and `shared/utils.js` directly.

Used by: smoke, load, stress, soak scenarios for midaz, console, pix.

**Pattern B: Palantir SDK (for complex scenarios with fixtures)**

For scenarios that need declarative fixture setup (create rules, limits, etc.),
sanity checks, and built-in metric tracking, use the Palantir SDK:

```javascript
import { scenario, fixture, createTestExports } from '../../../../shared/palantir/index.js';
```

Used by: tracer scenarios (pass-through, denied-by-limit, denied-by-rule, complex-approval).

**Choose Pattern A** unless the product requires setup fixtures (rules, limits, etc.) that
must be created and activated before load can run.

## Step 3: Create Product Files

### 3a. product.yaml

Create `platform/products/{product}/product.yaml`:

```yaml
product: {product}
description: "{Product description} - performance tests"
base_url_env: {PRODUCT}_BASE_URL

defaults:
  thresholds:
    http_req_duration: ["p(95)<500", "p(99)<1000"]
    http_req_failed: ["rate<0.01"]
  env:
    API_VERSION: "v1"

tags:
  - {product}
  - {relevant-tags}
```

### 3b. helpers/client.js

Create `platform/products/{product}/helpers/client.js`:

For **bearer auth** (most products):

```javascript
import http from 'k6/http';
import { getAuthHeaders } from '../../../shared/auth.js';

const BASE_URL = __ENV.{PRODUCT}_BASE_URL || __ENV.TARGET_URL || 'http://localhost:{base_port}';
const API_VERSION = __ENV.API_VERSION || 'v1';

export function apiUrl(path) {
  return `${BASE_URL}/${API_VERSION}${path}`;
}

export function get(path, params = {}) {
  const { headers: extraHeaders, ...restParams } = params;
  return http.get(apiUrl(path), {
    ...restParams,
    headers: { ...getAuthHeaders(), ...extraHeaders },
  });
}

export function post(path, body, params = {}) {
  const { headers: extraHeaders, ...restParams } = params;
  return http.post(apiUrl(path), JSON.stringify(body), {
    ...restParams,
    headers: { ...getAuthHeaders(), ...extraHeaders },
  });
}

export function patch(path, body, params = {}) {
  const { headers: extraHeaders, ...restParams } = params;
  return http.patch(apiUrl(path), JSON.stringify(body), {
    ...restParams,
    headers: { ...getAuthHeaders(), ...extraHeaders },
  });
}

export function del(path, params = {}) {
  const { headers: extraHeaders, ...restParams } = params;
  return http.del(apiUrl(path), null, {
    ...restParams,
    headers: { ...getAuthHeaders(), ...extraHeaders },
  });
}
```

For **API key auth** (e.g., tracer):

```javascript
import http from 'k6/http';

const BASE_URL = __ENV.{PRODUCT}_BASE_URL || __ENV.TARGET_URL || 'http://localhost:{base_port}';
const API_VERSION = __ENV.API_VERSION || 'v1';

function getHeaders() {
  const headers = { 'Content-Type': 'application/json' };
  const apiKey = __ENV.{PRODUCT}_API_KEY;
  if (apiKey) {
    headers['{api_key_header}'] = apiKey;
  }
  return headers;
}

export function apiUrl(path) {
  return `${BASE_URL}/${API_VERSION}${path}`;
}

// ... same get/post/patch/del pattern with getHeaders() ...

export function readiness() {
  return http.get(`${BASE_URL}/health`, {
    tags: { name: '{product}_readiness' },
  });
}
```

**Key rules for the client:**
- `__ENV.TARGET_URL` is the primary URL injected by Palantir SST — always include as fallback
- Product-specific env var (`{PRODUCT}_BASE_URL`) allows override in multi-product environments
- Never hardcode auth credentials — read from `__ENV`

## Step 4: Create Scenario Files

### 4a. scenario.yaml (per scenario)

Create `platform/products/{product}/scenarios/{type}/scenario.yaml`:

```yaml
name: "{Scenario Display Name}"
description: "{What this scenario validates}"
type: {smoke|load|stress|soak|functional}
tags: [{type}, {relevant-tags}]

defaults:
  vus: {default_vus}
  duration: "{default_duration}"
  parallelism: 1

params:
  - name: VUS
    label: "Virtual Users"
    type: number
    default: "{default_vus}"
    description: "Number of concurrent virtual users"
  - name: DURATION
    label: "Test Duration"
    type: string
    default: "{default_duration}"
    description: "How long the test runs (e.g. 1m, 5m, 30s)"
```

**Rules:**
- Every `params[].name` MUST match a `__ENV.XXX` variable read in test.js
- `type` must be one of: smoke, load, stress, soak, breakpoint, capacity, functional
- `defaults` define what Palantir pre-fills in the form

### Default values per scenario type

| Type | VUs | Duration | Thresholds |
|----

Related in Backend & APIs