Skip to main content
Back to blog
tutorials 30 November 2021 5 min read

GitHub Actions for Performance Testing: Automated Load Tests in CI/CD

Set up automated performance testing in GitHub Actions using k6 and JMeter, with result storage and performance regression detection.

M

Mark

Performance Testing Expert

Integrating performance testing into CI/CD pipelines catches regressions before they reach production. GitHub Actions provides a straightforward way to run load tests automatically on every deployment or on a schedule. Here’s how to set it up with both k6 and JMeter.

Basic k6 Workflow

k6 is particularly well-suited for GitHub Actions due to its single binary distribution and low resource requirements.

Create .github/workflows/performance-test.yml:

name: Performance Tests

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 6 * * *'  # Daily at 6 AM UTC
  workflow_dispatch:      # Manual trigger

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

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Install k6
        run: |
          sudo gpg -k
          sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
          echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
          sudo apt-get update
          sudo apt-get install k6

      - name: Run load test
        run: k6 run tests/load-test.js --out json=results.json

      - name: Upload results
        uses: actions/upload-artifact@v3
        with:
          name: k6-results
          path: results.json

Using the Official k6 Action

The Grafana team maintains an official action that simplifies setup:

jobs:
  load-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Run k6 load test
        uses: grafana/k6-action@v0.3.0
        with:
          filename: tests/load-test.js
          flags: --out json=results.json

      - name: Upload results
        uses: actions/upload-artifact@v3
        with:
          name: k6-results
          path: results.json

k6 Test Script with Thresholds

Create tests/load-test.js with pass/fail thresholds:

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  stages: [
    { duration: '1m', target: 20 },
    { duration: '3m', target: 20 },
    { duration: '1m', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],  // 95% under 500ms
    http_req_failed: ['rate<0.01'],    // <1% failure rate
    checks: ['rate>0.99'],             // 99% checks pass
  },
};

export default function () {
  const res = http.get(`${__ENV.TARGET_URL}/api/health`);

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time OK': (r) => r.timings.duration < 500,
  });
}

Pass the target URL as an environment variable:

- name: Run k6 load test
  uses: grafana/k6-action@v0.3.0
  with:
    filename: tests/load-test.js
  env:
    TARGET_URL: https://staging.example.com

JMeter in GitHub Actions

JMeter requires more setup but offers broader protocol support.

name: JMeter Performance Tests

on:
  workflow_dispatch:
  schedule:
    - cron: '0 2 * * 1'  # Weekly on Monday at 2 AM

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

    steps:
      - uses: actions/checkout@v3

      - name: Set up JDK
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'

      - name: Download JMeter
        run: |
          wget -q https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-5.5.tgz
          tar -xzf apache-jmeter-5.5.tgz

      - name: Run JMeter test
        run: |
          ./apache-jmeter-5.5/bin/jmeter \
            -n \
            -t tests/load-test.jmx \
            -l results.jtl \
            -e \
            -o report

      - name: Upload JTL results
        uses: actions/upload-artifact@v3
        with:
          name: jmeter-jtl
          path: results.jtl

      - name: Upload HTML report
        uses: actions/upload-artifact@v3
        with:
          name: jmeter-report
          path: report

Storing Results for Trend Analysis

To track performance over time, store results in a persistent location:

- name: Store results in S3
  uses: jakejarvis/s3-sync-action@master
  with:
    args: --acl public-read
  env:
    AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    SOURCE_DIR: 'results'
    DEST_DIR: 'performance/${{ github.sha }}'

Or use GitHub’s built-in artifact storage for simpler setups:

- name: Download previous results
  uses: dawidd6/action-download-artifact@v2
  with:
    workflow: performance-test.yml
    name: k6-results
    path: previous-results
  continue-on-error: true

- name: Compare results
  run: |
    if [ -f previous-results/results.json ]; then
      python scripts/compare-results.py previous-results/results.json results.json
    fi

Performance Regression Detection

Create a comparison script that fails the build on regressions:

#!/usr/bin/env python3
# scripts/compare-results.py
import json
import sys

def load_metrics(filepath):
    with open(filepath) as f:
        data = json.load(f)
    # Extract p95 response time from k6 JSON output
    metrics = [m for m in data if m.get('metric') == 'http_req_duration']
    p95_values = [m['data']['value'] for m in metrics
                  if m.get('data', {}).get('tags', {}).get('percentile') == '95']
    return sum(p95_values) / len(p95_values) if p95_values else None

previous = load_metrics(sys.argv[1])
current = load_metrics(sys.argv[2])

if previous and current:
    regression = ((current - previous) / previous) * 100
    print(f"Previous p95: {previous:.2f}ms")
    print(f"Current p95: {current:.2f}ms")
    print(f"Change: {regression:+.1f}%")

    if regression > 10:  # 10% regression threshold
        print("ERROR: Performance regression detected!")
        sys.exit(1)

Testing Against Different Environments

Use matrix builds to test multiple environments:

jobs:
  load-test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [staging, production]
        include:
          - environment: staging
            url: https://staging.example.com
            users: 50
          - environment: production
            url: https://api.example.com
            users: 10

    steps:
      - uses: actions/checkout@v3

      - name: Run k6 load test
        uses: grafana/k6-action@v0.3.0
        with:
          filename: tests/load-test.js
          flags: --vus ${{ matrix.users }} --duration 5m
        env:
          TARGET_URL: ${{ matrix.url }}

Posting Results to Pull Requests

Add test results as PR comments:

- name: Post results to PR
  if: github.event_name == 'pull_request'
  uses: actions/github-script@v6
  with:
    script: |
      const fs = require('fs');
      const results = fs.readFileSync('results-summary.txt', 'utf8');
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: `## Performance Test Results\n\`\`\`\n${results}\n\`\`\``
      });

Resource Limits and Timeouts

GitHub Actions runners have limited resources. For larger tests:

jobs:
  load-test:
    runs-on: ubuntu-latest
    timeout-minutes: 30  # Prevent runaway tests

    steps:
      - name: Run load test
        run: k6 run --vus 100 --duration 10m tests/load-test.js

For tests requiring more resources, consider self-hosted runners or cloud-based testing services.

Secrets and Sensitive Endpoints

Never hardcode credentials. Use GitHub Secrets:

env:
  API_KEY: ${{ secrets.LOAD_TEST_API_KEY }}
  TARGET_URL: ${{ secrets.STAGING_URL }}

And reference them in your test script:

const apiKey = __ENV.API_KEY;
const headers = { 'Authorization': `Bearer ${apiKey}` };

Automated performance testing in CI/CD provides early warning of regressions and builds confidence in deployments. Start with simple tests and thresholds, then expand coverage as you learn what metrics matter most for your application.

Tags:

#github-actions #ci-cd #automation #performance-testing

The cheat sheets, as printable PDFs

All 52 are free to read on the site — no signup. Want them as A4 PDFs you can actually print? The pack covers JMeter, k6, Gatling, Docker, Kubernetes and observability, and costs nothing but an email address.

Get the PDF pack

Delivered by email. Unsubscribe any time — see our privacy policy.

Stuck on this in your own test suite?

I take on fixed-price, fixed-turnaround performance work — script migrations, HAR-to-script conversion and written performance audits. No calls required.

Get in Touch