Artillery Advanced Scenarios: Custom Plugins, Processors, and Multi-Phase Load Tests
Artillery covers the basics well — YAML config, HTTP scenarios, simple ramp patterns. The advanced features are where it earns its place for complex load testing: custom JavaScript processors, stateful sessions, WebSocket testing, multi-environment configs, and plugin integration. This guide covers them all.
Custom Processors
Custom processors let you write JavaScript to handle logic that YAML can't express:
# artillery.yml
config:
target: "https://api.example.com"
processor: "./processor.js"
phases:
- duration: 60
arrivalRate: 50
scenarios:
- name: "Authenticated user flow"
flow:
- function: "generateAuthToken"
- post:
url: "/api/orders"
json:
amount: "{{ orderAmount }}"
headers:
Authorization: "Bearer {{ authToken }}"
capture:
- json: "$.id"
as: "orderId"
- function: "validateOrder"
- get:
url: "/api/orders/{{ orderId }}"
expect:
- statusCode: 200// processor.js
const crypto = require('crypto');
module.exports = {
generateAuthToken,
validateOrder,
generateUniqueEmail,
};
function generateAuthToken(context, events, done) {
// Generate a test JWT (or fetch a real one from auth service)
const payload = {
sub: `test-user-${context.vars.$uuid}`,
exp: Math.floor(Date.now() / 1000) + 3600,
};
context.vars.authToken = Buffer.from(JSON.stringify(payload)).toString('base64');
context.vars.orderAmount = (Math.random() * 100 + 10).toFixed(2);
return done();
}
function validateOrder(context, events, done) {
const orderId = context.vars.orderId;
if (!orderId) {
// Order creation failed — record custom metric
events.emit('counter', 'order.creation.failed', 1);
return done(new Error('Order ID not captured'));
}
events.emit('counter', 'order.creation.success', 1);
return done();
}
function generateUniqueEmail(context, events, done) {
context.vars.email = `test+${Date.now()}-${Math.random().toString(36).slice(2)}@example.com`;
return done();
}Stateful Sessions with CSV Data
Use real-world data from CSV files:
# test-users.csv
email,password,role
alice@test.com,password123,admin
bob@test.com,password456,user
carol@test.com,password789,userconfig:
target: "https://app.example.com"
payload:
path: "./test-users.csv"
fields:
- email
- password
- role
order: sequence # or random
skipHeader: true
scenarios:
- name: "User login flow"
flow:
- post:
url: "/api/auth/login"
json:
email: "{{ email }}"
password: "{{ password }}"
capture:
- json: "$.token"
as: "token"
- json: "$.userId"
as: "userId"
- get:
url: "/api/users/{{ userId }}/profile"
headers:
Authorization: "Bearer {{ token }}"
expect:
- statusCode: 200
- hasProperty: "email"Multi-Phase Load Scenarios
Model realistic traffic patterns:
config:
target: "https://api.example.com"
phases:
# Warmup: gradual traffic increase
- name: "Warmup"
duration: 120
arrivalRate: 5
rampTo: 50
# Sustained load: stable plateau
- name: "Sustained"
duration: 300
arrivalRate: 50
# Peak: simulate traffic spike
- name: "Peak"
duration: 60
arrivalRate: 50
rampTo: 200
# Spike: sudden burst
- name: "Spike"
duration: 30
arrivalRate: 500
# Recovery: drop back to normal
- name: "Recovery"
duration: 120
arrivalRate: 500
rampTo: 50
# Cooldown
- name: "Cooldown"
duration: 60
arrivalRate: 50
rampTo: 0Multiple Scenario Weights
Test different user behaviors at different ratios:
scenarios:
- name: "Browse products"
weight: 50 # 50% of virtual users
flow:
- get:
url: "/products"
- get:
url: "/products/{{ $randomNumber(1, 1000) }}"
- name: "Search and buy"
weight: 30 # 30% of virtual users
flow:
- get:
url: "/search?q={{ $randomString() }}"
- post:
url: "/cart/add"
json:
productId: "{{ $randomNumber(1, 1000) }}"
- post:
url: "/checkout"
- name: "Just browse"
weight: 20 # 20% of virtual users
flow:
- get:
url: "/products"
- think: 5 # pause 5 seconds (realistic user behavior)
- get:
url: "/about"WebSocket Load Testing
config:
target: "ws://chat.example.com"
phases:
- duration: 60
arrivalRate: 10
scenarios:
- name: "WebSocket chat session"
engine: "ws"
flow:
# Connect
- send: '{"action": "join", "room": "general"}'
# Wait for join confirmation
- think: 1
# Send messages
- loop:
- send: '{"action": "message", "text": "Hello from load test {{ $uuid }}"}'
- think: 2
count: 5
# Leave
- send: '{"action": "leave"}'For Socket.IO:
config:
target: "http://socketio.example.com"
phases:
- duration: 60
arrivalRate: 20
scenarios:
- name: "Socket.IO session"
engine: "socketio"
flow:
- emit:
channel: "join-room"
data: "test-room-{{ $randomNumber(1, 10) }}"
- think: 1
- emit:
channel: "message"
data:
text: "Load test message"
user: "bot-{{ $uuid }}"
- think: 5Custom Metrics and Assertions
// processor.js
module.exports = { trackBusinessMetrics };
function trackBusinessMetrics(context, events, done) {
const responseTime = context.vars.responseTime;
const endpoint = context.vars.endpoint;
// Custom histogram
events.emit('histogram', `response_time.${endpoint}`, responseTime);
// Custom counter
if (responseTime > 500) {
events.emit('counter', 'slow_responses', 1);
}
// Custom rate
events.emit('rate', 'requests_completed', 1);
return done();
}config:
ensure:
# Fail if these thresholds are exceeded
thresholds:
- http.response_time.p99: 500 # p99 < 500ms
- http.response_time.p95: 200 # p95 < 200ms
- http.request_rate: 100 # at least 100 req/s
- errors.rate: 1 # error rate < 1%Environment-Specific Configuration
# base.yml
config:
http:
timeout: 30
pool: 10
defaults:
headers:
Content-Type: "application/json"
scenarios:
- name: "API flow"
flow:
- get:
url: "/api/health"
---
# staging.yml
config:
target: "https://staging.api.example.com"
phases:
- duration: 60
arrivalRate: 10
---
# production.yml
config:
target: "https://api.example.com"
phases:
- duration: 300
arrivalRate: 100Run with environment overrides:
# Staging
artillery run --config staging.yml base.yml
# Production
artillery run --config production.yml base.yml
# Or with environment variables
ARTILLERY_TARGET=https://api.example.com artillery run base.ymlPlugins
artillery-plugin-expect
Validate response content beyond status codes:
config:
plugins:
expect: {}
scenarios:
- name: "User API"
flow:
- get:
url: "/api/users/1"
expect:
- statusCode: 200
- contentType: json
- hasProperty: "email"
- matchesRegexp: '"email": ".+@.+"'
- notHasProperty: "password"artillery-plugin-metrics-by-endpoint
Break down metrics per endpoint:
config:
plugins:
metrics-by-endpoint:
useOnlyRequestNames: true
scenarios:
- name: "Mixed API"
flow:
- get:
url: "/api/users"
name: "List users"
- get:
url: "/api/products"
name: "List products"
- post:
url: "/api/orders"
name: "Create order"Report will show p99, p95, error rate per endpoint instead of aggregated.
artillery-plugin-publish-metrics
Send metrics to external systems:
config:
plugins:
publish-metrics:
- type: datadog
apiKey: "{{ $env.DD_API_KEY }}"
prefix: "load_test"
tags:
- "env:staging"
- "test:checkout_flow"CI/CD Integration
GitHub Actions
# .github/workflows/load-test.yml
name: Load Test
on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
workflow_dispatch:
inputs:
environment:
type: choice
options: [staging, production]
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Artillery
run: npm install -g artillery@latest
- name: Run Load Test
run: |
artillery run \
--config tests/load/${{ inputs.environment || 'staging' }}.yml \
--output report.json \
tests/load/scenarios.yml
env:
API_KEY: ${{ secrets.API_KEY }}
- name: Generate HTML Report
if: always()
run: artillery report report.json --output report.html
- name: Upload Report
if: always()
uses: actions/upload-artifact@v4
with:
name: load-test-report
path: |
report.json
report.html
- name: Check Thresholds
run: |
# Fail if p99 > 500ms or error rate > 1%
node scripts/check-thresholds.js report.jsonThreshold Check Script
// scripts/check-thresholds.js
const report = require(process.argv[2]);
const summary = report.aggregate;
const p99 = summary.latency.p99;
const errorRate = summary.errors.rate;
const thresholds = {
p99: 500,
errorRate: 1,
};
let failed = false;
if (p99 > thresholds.p99) {
console.error(`❌ p99 latency ${p99}ms exceeds threshold ${thresholds.p99}ms`);
failed = true;
} else {
console.log(`✓ p99 latency ${p99}ms within threshold`);
}
if (errorRate > thresholds.errorRate) {
console.error(`❌ Error rate ${errorRate}% exceeds threshold ${thresholds.errorRate}%`);
failed = true;
} else {
console.log(`✓ Error rate ${errorRate}% within threshold`);
}
process.exit(failed ? 1 : 0);Debugging Artillery Tests
# Debug mode — see every request and response
artillery run --debug scenarios.yml 2>&1 | head -100
# Verbose output
artillery run --verbose scenarios.yml
# Dry run — validate config without sending requests
artillery run --dry-run scenarios.yml
# Quick smoke test — 1 VU, 10 seconds
artillery quick --count 10 --num 1 https://api.example.com/health
# Record a session for replay (artillery pro)
artillery record --output recorded.yml https://app.example.comSummary
Artillery's advanced features enable production-grade load testing:
- Custom processors handle auth, data generation, and custom metrics
- CSV payloads test with real-world user data
- Multi-phase scenarios model traffic patterns accurately
- WebSocket support tests real-time application performance
- Plugins extend reporting, expectations, and integrations
- CI integration makes load testing part of every release
The goal is load tests that reflect production reality closely enough that when they pass, you can deploy with confidence.