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.mdanddocs/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:
- Registers with the dashboard (
POST /api/k6/agents/register). - Polls for its target list every
POLL_INTERVAL_MS(default 5 s). - 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 - Parses the JSON summary and POSTs structured metrics to the dashboard.
- 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)
kubectlconfigured 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
HPAScaleToZerofeature 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. - Prometheus —
reaper_k6agents_marked_offline_total,reaper_k6agents_deleted_totalfrom the dashboard/api/metricsendpoint. - Grafana — import the included dashboard JSON (see
dashboards/once published; planned JSON toobservability/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) |