Skip to content

K6 load-test fleet — operations guide

Scope status (post-Scope-Freeze 2026-05-10) — See ARCHITECTURE.md for the canonical 37 MÓDULOs + 7 Test Kinds + DOM/CPOS/PIE-PA safety architecture. ADRs 0014, 0019-0025 cover post-Freeze additions.

Companion to docs/SPLIT_STACKS.md and docs/ARCHITECTURE.md. Architecture decision: docs/ADR/0006-k6-load-test-fleet.md.

What the K6 fleet does

Each K6 agent is a lightweight TypeScript + Node.js process (~128 MB RAM) that wraps the official grafana/k6 binary. It:

  1. Registers with the dashboard (POST /api/k6/agents/register).
  2. Polls for its target list every POLL_INTERVAL_MS (default 5 s).
  3. For each enabled target whose run interval has elapsed, spawns:
    k6 run --summary-export /tmp/summary-{runId}.json \
           --no-usage-report --no-color \
           /tmp/k6-script-{runId}.js
    
  4. Parses the JSON summary and POSTs structured metrics to the dashboard.
  5. Sends a heartbeat every HEARTBEAT_INTERVAL_MS (default 30 s).

Metrics captured per run:

Metric Source field in summary JSON
p50_ms http_req_duration.values.med
p95_ms http_req_duration.values["p(95)"]
p99_ms http_req_duration.values["p(99)"]
avg_ms http_req_duration.values.avg
error_rate http_req_failed.values.rate
rps derived from http_reqs.values.count / duration
data_received_bytes data_received.values.count
iterations iterations.values.count

vs. Playwright fleet

Playwright fleet K6 fleet
Measures Full page load, per-resource metrics, TLS version, Web Vitals HTTP transport latency percentiles, error rate, RPS
Browser Real Chromium (headless) None — k6 Go HTTP stack
Max agents (Compose) 300 1,000
Max agents (K8s HPA) 300 1,000 (0 with scale-to-zero)
RAM / agent ~300–500 MiB ~128 MiB
JavaScript execution Yes No
Load model One full-page cycle per agent per interval N virtual users for D seconds per agent per interval
Use case "Does the site work for a real user?" "How does the site behave under load?"

Use both fleets together: Playwright for correctness, K6 for capacity.

Quick start

# 1. Bring up all 5 stacks (K6 fleet starts with 0 agents)
scripts/stack-up.sh up

# 2. Add a K6 target via the dashboard UI at http://localhost:3000/agents/k6/sites
#    Or via the API:
ADMIN=$(grep ^ADMIN_BASIC_AUTH .env | cut -d= -f2-)
curl -u "$ADMIN" -X POST -H 'content-type: application/json' \
  -d '{"url":"https://example.com","label":"Example","vus":10,"durationS":30,"runIntervalMs":60000}' \
  http://localhost:3000/api/admin/k6/targets

# 3. Scale the K6 fleet to 3 agents
scripts/stack-up.sh scale-k6 3

# 4. Watch the Execuções tab at http://localhost:3000/agents/k6/runs

Compose commands

All K6 fleet commands go through scripts/stack-up.sh:

scripts/stack-up.sh up                 # start all 5 stacks (K6 at desired count)
scripts/stack-up.sh scale-k6 N        # scale K6 fleet to N agents (1 ≤ N ≤ 1,000)
scripts/stack-up.sh restart-k6        # bounce all K6 agents (control stays up)
scripts/stack-up.sh logs              # tail logs from all 5 stacks (Ctrl-C to stop)
scripts/stack-up.sh down              # stop all 5 stacks (preserves volumes)
scripts/stack-up.sh destroy           # stop + drop all volumes + networks

Or use Docker Compose directly:

docker compose -p ai_forse_k6 -f docker-compose.k6-fleet.yml up -d --scale k6agent=5
docker compose -p ai_forse_k6 -f docker-compose.k6-fleet.yml logs -f
docker compose -p ai_forse_k6 -f docker-compose.k6-fleet.yml down

Kubernetes deployment

Prerequisites

  • Kubernetes cluster with Metrics Server (for HPA)
  • kubectl configured for the cluster
  • Images pushed to GHCR or your registry

Apply manifests

# 1. Config + Secret (edit placeholders first)
kubectl apply -f k8s/11-k6-agent-config.yaml

# 2. Deployment + HPA + PDB
kubectl apply -f k8s/21-k6-agent-deployment.yaml

# 3. NetworkPolicy (egress allow-list)
kubectl apply -f k8s/31-k6-agent-network-policy.yaml

Or via Helm:

helm upgrade --install web-agent charts/web-agent-cluster \
  --set image.k6agent.tag=v3.0.0 \
  --set k6agent.dashboardSecret=<your-secret> \
  --set k6agent.replicas=5

Scale-to-zero (optional)

The K6 HPA has minReplicas: 0. This requires either:

  • KEDA — install the KEDA operator and it handles scale-to-zero for standard HPAs without additional CRDs.
  • Kubernetes ≥ 1.32 — enable the HPAScaleToZero feature gate.

On older clusters without KEDA, set minReplicas: 1 in values.yaml:

k6agent:
  hpa:
    minReplicas: 1

Resource sizing

Workload CPU request CPU limit Memory request Memory limit
Each k6agent pod 100m 500m 128Mi 256Mi
1,000 agents 100 vCPU 500 vCPU 128 GiB 256 GiB

In practice, idle agents use ~10m CPU and ~64 MiB RAM. Active agents (running k6 with 10 VUs) spike to ~100m CPU and ~160 MiB RAM.

Downward API — agent identity

Each K6 pod gets its name as AGENT_NAME via the Downward API:

- name: AGENT_NAME
  valueFrom:
    fieldRef:
      fieldPath: metadata.name

This means the dashboard sees k6agent-0, k6agent-1, … without any external registration or UUID generation. If a pod restarts, it reuses the same name and the dashboard row is updated in place (upsert).

Read-only root filesystem

The K6 agent container runs with readOnlyRootFilesystem: true. It needs two writable locations:

Path Purpose Volume type
/tmp k6 script file + summary JSON per run emptyDir: medium: Memory (256 MiB)
/home/k6agent Node.js home directory emptyDir (64 MiB)

Both are memory-backed so the container never touches the node's filesystem. In Compose, equivalent tmpfs mounts are used.

Configuring targets

Via the UI

Navigate to http://localhost:3000/agents/k6/sites and click Adicionar.

Field Description Default
URL HTTPS URL to test
Label Human-readable name (empty)
VUs Virtual users per run 1
Duration (s) k6 test duration in seconds 30
Run interval (ms) How often each agent fires this target 60,000
Enabled Whether agents pick up this target true

Via the API

ADMIN=$(grep ^ADMIN_BASIC_AUTH .env | cut -d= -f2-)
BASE=http://localhost:3000

# Create
curl -u "$ADMIN" -X POST -H 'content-type: application/json' \
  -d '{"url":"https://example.com","label":"Example","vus":10,"durationS":30,"runIntervalMs":60000}' \
  $BASE/api/admin/k6/targets

# List (read-only, no auth)
curl $BASE/api/dashboard/k6/targets

# Update (disable)
curl -u "$ADMIN" -X PATCH -H 'content-type: application/json' \
  -d '{"enabled":false}' \
  $BASE/api/admin/k6/targets/{id}

# Delete
curl -u "$ADMIN" -X DELETE $BASE/api/admin/k6/targets/{id}

Scaling the fleet

Via the UI

Open /agents/k6 and drag the Escala K6 slider (0–1,000) or click a preset (0, 1, 5, 10, 50, 100, 500, 1,000). Click Reconciliar to force an immediate Docker Compose scale operation.

Via the API

ADMIN=$(grep ^ADMIN_BASIC_AUTH .env | cut -d= -f2-)
curl -u "$ADMIN" -X PUT -H 'content-type: application/json' \
  -d '{"k6DesiredAgentCount":20}' \
  http://localhost:3000/api/admin/config

Via the script

scripts/stack-up.sh scale-k6 20

Observability

K6 run metrics are stored in the k6_runs table and exposed at:

  • Dashboard/agents/k6/runs — paginated table with p95 latency, fail rate (red when > 5 %), per-target filtering.
  • Prometheusreaper_k6agents_marked_offline_total, reaper_k6agents_deleted_total from the dashboard /api/metrics endpoint.
  • Grafana — import the included dashboard JSON (see dashboards/ once published; planned JSON to observability/grafana/dashboards/).

Reaper lifecycle

The background reaper (every 30 s) manages K6 agent lifecycle:

Condition Action
Run status='running' for > 5 minutes Set status='error', set error_message
Agent has current_run_id pointing to a non-running run Clear current_run_id
Agent last_seen_at > 5 minutes ago Set status='offline'
Agent status='offline' and last_seen_at > 1 hour ago DELETE row (cascade to runs)

The 1-hour GC prevents the k6_agents table from accumulating zombie rows when agents crash-loop and register a new name on each restart.

Local development

# Build the k6-agent image locally
docker build -t ghcr.io/nollagluiz/web-agent-k6agent:dev ./k6-agent

# Run with the dev-build override
docker compose -p ai_forse_k6 \
  -f docker-compose.k6-fleet.yml \
  -f docker-compose.dev-build.yml \
  up -d k6agent

# Watch logs
docker compose -p ai_forse_k6 -f docker-compose.k6-fleet.yml logs -f k6agent

Environment variables

All variables are set in docker-compose.k6-fleet.yml (Compose) or k8s/11-k6-agent-config.yaml (Kubernetes).

Variable Default Description
DASHBOARD_URL http://dashboard:3000 Base URL of the dashboard API
DASHBOARD_SECRET Bearer token matching dashboard's DASHBOARD_SECRET
AGENT_NAME Unique agent identifier (set by Downward API in K8s)
POLL_INTERVAL_MS 5000 How often to fetch the target list
HEARTBEAT_INTERVAL_MS 30000 How often to send a heartbeat
K6_BINARY /usr/bin/k6 Path to the k6 binary
TMP_DIR /tmp Directory for temporary script and summary files
K6_VUS 1 Default virtual users (overridden per target)
K6_DURATION 30s Default duration (overridden per target)