Introduction to Artillery

Artillery is a modern, powerful load testing toolkit designed for developers. With YAML-based scenario definitions, built-in support for HTTP, WebSocket (as discussed in Gatling: High-Performance Load Testing with Scala DSL), and Socket.io, plus a rich plugin ecosystem, Artillery excels at testing modern, real-time applications.

Basic YAML Scenario

# load-test.yml
config:
  target: "https://api.example.com"
  phases:
    - duration: 60
      arrivalRate: 10  # 10 users per second
      name: "Warm up"
    - duration: 120
      arrivalRate: 50  # Ramp to 50 users/sec
      name: "Sustained load"
    - duration: 30
      arrivalRate: 100  # Peak load
      name: "Spike test"

  processor: "./flows.js"  # Custom JavaScript logic

scenarios:
  - name: "Browse and purchase"
    flow:
      - get:
          url: "/api/products"
          capture:
            - json: "$[0].id"
              as: "productId"

      - post:
          url: "/api/cart"
          json:
            productId: "{{ productId }}"
            quantity: 1
          capture:
            - json: "$.cartId"
              as: "cartId"

      - post:
          url: "/api/checkout"
          json:
            cartId: "{{ cartId }}"
            paymentMethod: "credit_card"
          expect:
            - statusCode: 200
            - contentType: json
            - hasProperty: orderId

Advanced Features

Custom JavaScript Processor

// flows.js
module.exports = {
  setAuthToken,
  generateTestData,
  validateResponse
};

function setAuthToken(requestParams, context, ee, next) {
  // Login to get token
  const request = require('request');

  request.post({
    url: `${context.vars.target}/auth/login`,
    json: {
      username: 'test@example.com',
      password: 'password123'
    }
  }, (err, res, body) => {
    if (!err && res.statusCode === 200) {
      context.vars.authToken = body.token;
    }
    return next();
  });
}

function generateTestData(context, events, done) {
  // Generate random test data
  context.vars.username = `user_${Date.now()}`;
  context.vars.email = `${context.vars.username}@example.com`;
  return done();
}

function validateResponse(requestParams, response, context, ee, next) {
  // Custom validation logic
  const body = JSON.parse(response.body);

  if (body.status !== 'success') {
    ee.emit('error', 'Invalid response status');
  }

  if (response.timings.phases.total > 1000) {
    console.log(`Slow response: ${response.timings.phases.total}ms`);
  }

  return next();
}

WebSocket Testing

# websocket-test.yml
config:
  target: "ws://localhost:8080"
  phases:
    - duration: 60
      arrivalRate: 10

scenarios:
  - engine: ws
    flow:
      # Connect to WebSocket
      - send: '{"action": "subscribe", "channel": "prices"}'

      # Wait for message
      - think: 1

      # Send message and expect response
      - send: '{"action": "get_price", "symbol": "BTC"}'
      - match:
          - json: "$.symbol"
            value: "BTC"
          - json: "$.price"
            exists: true

      # Loop messages
      - loop:
        - send: '{"action": "ping"}'
        - think: 5
        count: 10

Plugin Ecosystem

Metrics Plugins

# artillery.yml with plugins
config:
  target: "https://api.example.com"
  plugins:
    # Publish metrics to CloudWatch
    cloudwatch:
      region: "us-east-1"
      namespace: "LoadTests"

    # Publish to Datadog
    datadog:
      apiKey: "{{ $processEnvironment.DD_API_KEY }}"
      tags:
        - "env:production"
        - "team:backend"

    # Publish to Prometheus
    publish-metrics:
      - type: prometheus
        pushgateway: "http://prometheus:9091"

  phases:
    - duration: 300
      arrivalRate: 20

scenarios:
  - flow:
      - get:
          url: "/api/metrics"

Expect Plugin

config:
  target: "https://api.example.com"
  plugins:
    expect: {}  # Enable expect plugin

scenarios:
  - name: "API validation"
    flow:
      - get:
          url: "/api/users/{{ userId }}"
          expect:
            - statusCode: 200
            - contentType: json
            - hasHeader: "x-request-id"
            - equals:
                - "{{ $. username}}"
                - "john_doe"
            - matchesRegexp:
                - "{{ $.email }}"
                - "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"

CI/CD Integration

GitHub Actions

# .github/workflows/performance.yml
name: Performance Tests

on:
  schedule:
    - cron: '0 */6 * * *'  # Every 6 hours
  workflow_dispatch:

jobs:
  artillery-test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install Artillery
        run: npm install -g artillery@latest

      - name: Run Performance Test
        run: |
          artillery run \
            --output report.json \
            load-test.yml

      - name: Generate HTML Report
        run: artillery report report.json --output report.html

      - name: Check SLO Compliance
        run: |
          artillery report report.json \
            --assert \
            "p95 < 500" \
            "p99 < 1000" \
            "errors.rate < 0.01"

      - name: Upload Reports
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: performance-reports
          path: |
            report.json
            report.html

GitLab CI/CD

# .gitlab-ci.yml
performance-test:
  stage: test
  image: node:18
  script:
    - npm install -g artillery
    - artillery run --output results.json load-test.yml
    - artillery report results.json

  artifacts:
    reports:
      performance: results.json
    paths:
      - results.json

  only:
    - schedules
    - web

Best Practices

1. Realistic Load Profiles

config:
  phases:
    # Gradual ramp-up
    - duration: 120
      arrivalRate: 1
      rampTo: 50
      name: "Ramp up"

    # Sustained load
    - duration: 600
      arrivalRate: 50
      name: "Sustained"

    # Spike test
    - duration: 60
      arrivalRate: 200
      name: "Spike"

    # Cool down
    - duration: 60
      arrivalRate: 10
      name: "Cool down"

2. Environment-Specific Configs

# Base config
config:
  target: "{{ $processEnvironment.TARGET_URL }}"
  phases:
    - duration: "{{ $processEnvironment.DURATION }}"
      arrivalRate: "{{ $processEnvironment.ARRIVAL_RATE }}"

# Run with environment variables
# TARGET_URL=https://api.example.com DURATION=300 ARRIVAL_RATE=50 artillery run test.yml

3. Custom Metrics

// custom-metrics.js
module.exports = { trackBusinessMetrics };

function trackBusinessMetrics(userContext, events, done) {
  events.on('response', (data) => {
    const body = JSON.parse(data.body);

    // Track business-specific metrics
    if (body.orderValue) {
      events.emit('counter', 'orders.total', 1);
      events.emit('histogram', 'orders.value', body.orderValue);
    }

    if (body.discountApplied) {
      events.emit('counter', 'discounts.applied', 1);
    }
  });

  return done();
}

Artillery vs Other Tools

FeatureArtilleryJMeterLocustk6
Config FormatYAMLGUI/XMLPythonJavaScript
Learning CurveLowMedium-HighLow (Python)Low (JS)
WebSocketExcellentLimitedManualGood
Socket.ioNativeNoNoLimited
PluginsExcellentExtensiveLimitedGrowing
CI/CD (as discussed in K6: Modern Load Testing with JavaScript for DevOps Teams)ExcellentGoodExcellentExcellent
Real-time AppsExcellentPoorGoodGood
DistributedCloud (paid)Built-inBuilt-inCloud

Conclusion

Artillery excels at testing modern applications with its YAML-based scenarios, excellent WebSocket/Socket.io support, and rich plugin ecosystem. Its developer-friendly approach and strong CI/CD integration make it ideal for teams practicing continuous performance testing (as discussed in Locust Python Load Testing: Complete Performance Testing Guide).

Choose Artillery when:

  • Testing real-time applications (WebSocket, Socket.io)
  • YAML configuration preferred over code
  • Modern, developer-friendly tooling desired
  • Rich plugin ecosystem valuable
  • CI/CD integration is priority

Choose alternatives when:

  • Distributed testing without cloud service needed (Locust, k6)
  • GUI-based test creation preferred (JMeter)
  • Custom Python logic essential (Locust)
  • Advanced JavaScript scenarios needed (k6)

For teams building modern, real-time applications and practicing DevOps, Artillery provides an excellent balance of simplicity, power, and extensibility for performance testing.