Artillery Advanced Scenarios: Custom Plugins, Processors, and Multi-Phase Load Tests

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,user
config:
  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: 0

Multiple 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: 5

Custom 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: 100

Run 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.yml

Plugins

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.json

Threshold 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.com

Summary

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.

Read more

Start now free