Stop Shipping Slow Code: Automating Performance Testing in GitHub Actions

We’ve all been there: you merge a ‘minor’ optimization that looks great in local development, only to find out in production that your p99 latency has spiked by 200ms. For a long time, I treated performance testing as a ‘once-a-quarter’ event. But in a fast-paced CI/CD environment, that’s a recipe for disaster. Integrating performance testing in GitHub Actions allows you to shift performance left, catching regressions before they ever hit a staging environment.

In my experience, the biggest hurdle isn’t the testing tool itself, but the environment. Running a load test on a shared GitHub-hosted runner can lead to “noisy neighbor” effects, where your results fluctuate based on the VM’s current load rather than your code’s efficiency. In this tutorial, I’ll show you how to set up a reliable pipeline using k6, as it’s lightweight and scriptable in JavaScript.

Prerequisites

Before we dive into the YAML configuration, ensure you have the following ready:

Step-by-Step Implementation

Step 1: Write Your Performance Script

I recommend creating a performance/ directory in your root to keep your scripts separate from unit tests. Here is a sample k6 script that checks for a response time under 200ms for 95% of requests.

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

export const options = {
  thresholds: {
    http_req_duration: ['p(95)<200'], // 95% of requests must be below 200ms
  },
  vus: 10, // 10 virtual users
  duration: '30s',
};

export default function () {
  const res = http.get('https://staging-api.example.com/health');
  check(res, { 'status was 200': (r) => r.status == 200 });
  sleep(1);
}

Step 2: Configure the GitHub Actions Workflow

Now, we need to create a workflow file. I prefer using the k6-action because it handles the binary installation and result parsing seamlessly. Create a file at .github/workflows/performance.yml:

name: Performance Regression Test

on:
  pull_request:
    branches: [ main ]

jobs:
  performance-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Run k6 load test
        uses: k6-io/action@v2
        with:
          filename: performance/test-script.js
          flags: --vus 10 --duration 30s
GitHub Actions workflow run showing k6 performance test results and threshold failure
GitHub Actions workflow run showing k6 performance test results and threshold failure

As shown in the image below, once this workflow triggers, GitHub Actions will execute the k6 binary, run your virtual users against the target URL, and fail the build if the p(95) threshold is exceeded.

Step 3: Handling Environment Secrets

You should never hardcode your staging URLs or API keys in your scripts. Instead, use GitHub Secrets. Update your k6 script to use __ENV and your workflow to pass those secrets:

# In the workflow YAML
      - name: Run k6 load test
        uses: k6-io/action@v2
        with:
          filename: performance/test-script.js
          env: STAGING_URL: ${{ secrets.STAGING_URL }}

Pro Tips for Reliable Performance Testing

Troubleshooting Common Issues

Issue: Flaky tests (Non-deterministic results).
This is common in performance testing in GitHub Actions. If your tests fail randomly, check if you are hitting rate limits on your staging environment or if other CI jobs are running concurrently on the same target server.

Issue: Connection timeouts.
Ensure your staging environment’s firewall allows incoming traffic from GitHub’s IP ranges or use a tool like Tailscale to create a secure tunnel between the runner and your private server.

What’s Next?

Once you’ve mastered basic load testing, I suggest moving toward canary deployments. Instead of just testing in CI, you can use tools like ArgoCD or Flagger to shift a small percentage of real traffic to a new version and automatically roll back if performance metrics degrade.

Ready to scale? Check out my deep dive into scaling k6 tests with Grafana Cloud to get beautiful real-time dashboards for your GitHub Actions runs.