PerfLoad No script HTTP load testing

PerfLoad / GitHub Actions load testing

GitHub Actions Load Testing With a Saved Baseline Run

A GitHub Actions workflow can run a PerfLoad load test the same way it runs any other check: pull a saved test configuration, start a run, wait for it to finish, and read the numbers. Below is a working workflow that ends with p95 latency available as a step output for whatever should happen next.

Add Load Testing to GitHub Actions →

Needs a PerfLoad runner the workflow can reach — see runner networking below.

What the workflow does

  1. Retrieves a baseline configuration from the runner: GET /runs/<baseline id>, taking the .config object.
  2. Starts a new load test with POST /runs, overriding the virtual users.
  3. Waits for completion by polling GET /runs/<id>/status until it's finished, failed, or stopped.
  4. Retrieves the summary from GET /runs/<id>/summary.
  5. Exposes p95 latency by writing it to $GITHUB_OUTPUT, so later steps can read it as steps.load_test.outputs.p95.

The workflow

.github/workflows/load-test.yml
name: load-test

on:
  workflow_dispatch:
    inputs:
      users:
        description: Virtual users
        default: "50"

jobs:
  load-test:
    runs-on: ubuntu-latest
    env:
      RUNNER: ${{ vars.RUNNER_URL }}
      BASELINE_RUN_ID: ${{ vars.BASELINE_RUN_ID }}
      USERS: ${{ inputs.users || '50' }}
    steps:
      - name: Run load test
        id: load_test
        run: |
          # Reuse the baseline config, overriding virtual users
          CONFIG=$(curl -sf "$RUNNER/runs/$BASELINE_RUN_ID" \
            | jq --argjson users "$USERS" '.config | .users = $users')

          # Start a new run
          NEW_ID=$(curl -sf -X POST "$RUNNER/runs" \
            -H "Content-Type: application/json" \
            -d "$CONFIG" | jq -r '.id')
          echo "run_id=$NEW_ID" >> "$GITHUB_OUTPUT"

          # Wait for a terminal status
          while true; do
            STATUS=$(curl -sf "$RUNNER/runs/$NEW_ID/status" | jq -r '.status')
            [ "$STATUS" = "finished" ] || [ "$STATUS" = "failed" ] || [ "$STATUS" = "stopped" ] && break
            sleep 2
          done
          [ "$STATUS" = "finished" ] || { echo "Run ended as $STATUS"; exit 1; }

          # Read the summary and expose p95 to later steps
          SUMMARY=$(curl -sf "$RUNNER/runs/$NEW_ID/summary")
          echo "p95=$(jq '.metrics.http_req_duration.values["p(95)"]' <<< "$SUMMARY")" >> "$GITHUB_OUTPUT"
          jq '.metrics' <<< "$SUMMARY"

      - name: Show p95 latency
        env:
          P95: ${{ steps.load_test.outputs.p95 }}
        run: echo "p95 latency = ${P95} ms"

      - name: Fail if p95 is over budget
        env:
          P95: ${{ steps.load_test.outputs.p95 }}
          BUDGET_MS: "500"
        run: awk -v p="$P95" -v b="$BUDGET_MS" 'BEGIN { exit !(p != "" && p + 0 <= b + 0) }'

The load-test step follows the runner's documented CI/CD example, with three changes: variables come in through env rather than being interpolated into the script, the virtual-user count is a workflow input, and the step fails if the run doesn't end as finished. The final step is not a PerfLoad feature; it's a plain shell comparison against a 500 ms budget you'd tune for your own service. Latency in the summary is in milliseconds.

Setting it up

  1. Run the test you want in the PerfLoad Workbench or Dashboard and copy its Run ID.
  2. In your repository, add two variables under Settings → Secrets and variables → Actions: RUNNER_URL (for example http://my-runner:3000) and BASELINE_RUN_ID.
  3. Commit the workflow, then trigger it from the Actions tab.

Variables are fine for these two values, since neither is a credential. If your runner URL should stay private, store it as a secret and reference it with secrets.RUNNER_URL instead.

Where the runner lives matters

Jobs on GitHub-hosted runners start on the public internet, so RUNNER_URL must be reachable from there. If your PerfLoad runner sits in a private network next to the service you're testing, run the job on a self-hosted GitHub Actions runner inside that network instead. The PerfLoad runner API doesn't document built-in authentication, so avoid exposing it to the open internet; see self-hosted load testing for the deployment side.

Smoke on every change, load on demand

The users input is the simplest override. Extend the same jq expression to change duration as well — .config | .users = 5 | .duration = "30s" for a quick check on pull requests, and a longer profile for a manually dispatched pre-release run. Both replay the same saved baseline, so the request, headers, and variables stay identical between runs.

Using the p95 output

Anything that can read a step output can use it: a comment on the pull request, a line in the job summary, or the budget check in the workflow above. The step's other output, run_id, is there so a follow-up step can link to the run or, for example, request a comparison report from POST /runs/compare/report.pdf.

FAQ

Can I trigger this on every pull request?

You can change on: to pull_request. Since each run starts a k6 process on the runner and runs aren't queued, keep PR-triggered runs small (a few users, short duration), and reserve larger runs for manual or release triggers.

Will the baseline Run ID keep working after the runner is updated?

As long as run history is on persistent storage, such as the perfload-runs Docker volume or a Kubernetes PersistentVolumeClaim, yes. Updating the image doesn't touch it.

Is there a PerfLoad GitHub Action I can install?

The documented integration is the runner's REST API, called with curl and jq from a normal run: step, which is what the workflow above does. There is nothing to install in the workflow.

Related guides

Get p95 into your workflow

Save a baseline run, add two variables, and let Actions replay it.

Add Load Testing to GitHub Actions →