LoadForge LogoLoadForge

How to Run Load Testing in GitHub Actions with LoadForge

How to Run Load Testing in GitHub Actions with LoadForge

Introduction

Modern CI/CD pipelines don’t just validate correctness—they also need to validate performance. If your application passes unit and integration tests but slows down dramatically after a new deployment, you still have a production problem. That’s why adding load testing to GitHub Actions is such a practical step for engineering teams that want to catch performance regressions before code reaches users.

Running load testing in GitHub Actions with LoadForge gives you a repeatable, automated way to verify application behavior under load as part of every pull request, release branch, or scheduled workflow. Instead of treating performance testing as a one-off exercise, you can make it a standard quality gate in your CI pipeline.

This guide walks through how to run LoadForge load tests from GitHub Actions, what to test, how to structure Locust scripts for realistic CI-driven performance testing, and how to interpret the results. You’ll also see practical examples for public APIs, authenticated application flows, and regression-focused smoke load tests. Because LoadForge is cloud-based and built on Locust, you can execute distributed testing at scale without maintaining your own load generators, while still keeping your test logic in familiar Python.

Prerequisites

Before you start, make sure you have the following in place:

  • A LoadForge account
  • A GitHub repository with GitHub Actions enabled
  • A target application or API environment to test, such as:
    • a staging environment
    • a preview deployment
    • a pre-production environment
  • A LoadForge API token or integration credentials stored securely in GitHub Secrets
  • Basic familiarity with:
    • GitHub Actions workflows
    • HTTP APIs and authentication
    • Python and Locust scripting

You should also define what kind of performance checks you want in CI. Common examples include:

  • verifying a key endpoint still responds under moderate load
  • detecting latency regressions on pull requests
  • validating login and checkout workflows before release
  • running scheduled stress testing against staging overnight

A typical GitHub Actions setup will use secrets like:

  • LOADFORGE_API_TOKEN
  • LOADFORGE_TEST_ID
  • STAGING_BASE_URL
  • TEST_USERNAME
  • TEST_PASSWORD

These secrets should never be hardcoded in workflow files or Locust scripts.

Understanding GitHub Actions Under Load

GitHub Actions itself is not the system being load tested in most cases. Instead, GitHub Actions acts as the automation layer that triggers your LoadForge performance testing runs. The actual load is generated by LoadForge’s cloud-based infrastructure, which is important because GitHub-hosted runners are not suitable for generating meaningful distributed traffic at scale.

When teams first attempt load testing in CI/CD, they often make one of these mistakes:

  • generating load directly from the GitHub Actions runner
  • running tests against unstable ephemeral environments
  • testing unrealistic endpoints such as /health only
  • failing builds based on vague or missing performance thresholds

To use GitHub Actions effectively for load testing, think of it as an orchestrator. The workflow should:

  1. Deploy or identify the target environment
  2. Trigger a LoadForge test
  3. Wait for the results
  4. Evaluate pass/fail criteria
  5. Optionally publish metrics into the CI summary or notify the team

Common Bottlenecks Exposed by CI-Based Load Testing

When you automate performance testing in GitHub Actions, you’ll often uncover issues like:

  • slower database queries introduced in a pull request
  • authentication bottlenecks from token generation endpoints
  • rate-limiting misconfigurations in staging
  • insufficient autoscaling thresholds
  • increased response times in critical REST or GraphQL endpoints
  • memory or CPU regressions after dependency upgrades

CI-triggered load testing is especially useful for regression detection. You’re not always trying to simulate peak Black Friday traffic on every commit—you’re often trying to answer a simpler question: “Did this change make the app slower or less stable under expected concurrency?”

That’s where LoadForge shines. You can run distributed testing from global test locations, view real-time reporting as the test progresses, and integrate results into your CI/CD pipeline without managing any infrastructure.

Writing Your First Load Test

Let’s begin with a simple but realistic API load test you might run from GitHub Actions after a deployment to staging.

This first example simulates users browsing a product catalog and retrieving product details from a typical e-commerce API.

Basic API Regression Test

python
from locust import HttpUser, task, between
import random
 
class EcommerceApiUser(HttpUser):
    wait_time = between(1, 3)
 
    product_ids = [
        "sku_1001",
        "sku_1002",
        "sku_1003",
        "sku_1004",
        "sku_1005"
    ]
 
    @task(3)
    def list_products(self):
        self.client.get(
            "/api/v1/products?category=electronics&limit=20&sort=popular",
            name="/api/v1/products"
        )
 
    @task(2)
    def view_product_detail(self):
        product_id = random.choice(self.product_ids)
        self.client.get(
            f"/api/v1/products/{product_id}",
            name="/api/v1/products/:id"
        )
 
    @task(1)
    def search_products(self):
        self.client.get(
            "/api/v1/search?q=wireless+headphones&page=1",
            name="/api/v1/search"
        )

What This Test Does

This script represents a lightweight performance testing scenario for a staging API:

  • 3x more traffic goes to the product listing endpoint
  • 2x traffic goes to product detail pages
  • 1x traffic goes to search

That weighted behavior is more realistic than hammering a single URL repeatedly. It helps surface issues in caching, indexing, and backend query performance.

In LoadForge, you would configure:

  • host: your staging URL, such as https://staging.exampleapp.com
  • users: for example, 50
  • spawn rate: for example, 5 users per second
  • duration: for example, 5 to 10 minutes

This is a great first test to attach to a GitHub Actions workflow after deployment.

Example GitHub Actions Workflow

Here is a realistic workflow that runs after a staging deployment and triggers a LoadForge test.

yaml
name: Load Test Staging
 
on:
  workflow_dispatch:
  push:
    branches:
      - main
 
jobs:
  load-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
 
      - name: Trigger LoadForge test
        env:
          LOADFORGE_API_TOKEN: ${{ secrets.LOADFORGE_API_TOKEN }}
          LOADFORGE_TEST_ID: ${{ secrets.LOADFORGE_TEST_ID }}
        run: |
          curl -X POST "https://app.loadforge.com/api/v1/tests/${LOADFORGE_TEST_ID}/start/" \
            -H "Authorization: Token ${LOADFORGE_API_TOKEN}" \
            -H "Content-Type: application/json"

Depending on your setup, you may also want to poll the API and fail the workflow if thresholds are exceeded. We’ll discuss that pattern later in this guide.

Advanced Load Testing Scenarios

Basic endpoint testing is useful, but most applications need deeper performance testing scenarios in CI/CD. Below are several more advanced examples tailored to real application behavior and GitHub Actions-driven regression testing.

Authenticated User Flow with JWT Login

Many teams want to validate authenticated traffic in staging before merging or releasing. This example logs in via a real authentication endpoint, stores a bearer token, and exercises account-level API calls.

python
from locust import HttpUser, task, between
import random
 
class AuthenticatedApiUser(HttpUser):
    wait_time = between(1, 2)
 
    def on_start(self):
        response = self.client.post(
            "/api/v1/auth/login",
            json={
                "email": "loadtest.user@example.com",
                "password": "S3cureTestPass!"
            },
            name="/api/v1/auth/login"
        )
 
        if response.status_code == 200:
            token = response.json().get("access_token")
            self.client.headers.update({
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json"
            })
        else:
            response.failure(f"Login failed: {response.status_code} {response.text}")
 
    @task(3)
    def get_profile(self):
        self.client.get("/api/v1/account/profile", name="/api/v1/account/profile")
 
    @task(2)
    def list_orders(self):
        self.client.get("/api/v1/account/orders?page=1&page_size=10", name="/api/v1/account/orders")
 
    @task(1)
    def get_notifications(self):
        self.client.get("/api/v1/account/notifications?unread_only=true", name="/api/v1/account/notifications")
 
    @task(1)
    def update_preferences(self):
        payload = {
            "email_notifications": True,
            "sms_notifications": False,
            "preferred_currency": "USD",
            "theme": "dark"
        }
        self.client.put(
            "/api/v1/account/preferences",
            json=payload,
            name="/api/v1/account/preferences"
        )

Why This Scenario Matters in CI

Authentication often becomes a bottleneck under load. Teams frequently discover:

  • slow token issuance
  • session store contention
  • excessive DB lookups during login
  • degraded performance for authenticated endpoints after schema changes

This test is particularly valuable in GitHub Actions when tied to release branches or pre-production deployments. If a change increases median or p95 response time for login or account APIs, you can detect it before rollout.

Using Environment Variables in LoadForge Scripts

In real-world CI/CD pipelines, you usually don’t want credentials embedded in the script. A more production-ready pattern is to inject them through environment variables configured in LoadForge.

python
from locust import HttpUser, task, between
import os
 
class JwtApiUser(HttpUser):
    wait_time = between(1, 3)
 
    def on_start(self):
        email = os.getenv("LOADTEST_USERNAME", "loadtest.user@example.com")
        password = os.getenv("LOADTEST_PASSWORD", "S3cureTestPass!")
 
        response = self.client.post(
            "/api/v1/auth/login",
            json={
                "email": email,
                "password": password
            },
            name="/api/v1/auth/login"
        )
 
        if response.status_code != 200:
            response.failure("Authentication failed")
            return
 
        token = response.json()["access_token"]
        self.client.headers.update({
            "Authorization": f"Bearer {token}"
        })
 
    @task
    def get_dashboard(self):
        self.client.get("/api/v1/dashboard/summary", name="/api/v1/dashboard/summary")

This approach is better for GitHub Actions and LoadForge because secrets remain externalized and manageable across environments.

Database-Heavy Workflow: Cart and Checkout Preparation

A more advanced load testing scenario involves stateful operations that stress application logic, inventory checks, pricing engines, and database writes. This is exactly the kind of workflow that catches performance regressions in CI.

python
from locust import HttpUser, task, between
import random
import uuid
 
class CheckoutPreparationUser(HttpUser):
    wait_time = between(2, 5)
 
    inventory = ["sku_1001", "sku_1002", "sku_1003", "sku_2001", "sku_2002"]
 
    def on_start(self):
        guest_id = str(uuid.uuid4())
        self.client.headers.update({
            "X-Guest-Session": guest_id,
            "Content-Type": "application/json"
        })
 
    @task(2)
    def browse_category(self):
        self.client.get(
            "/api/v1/products?category=laptops&limit=24&page=1",
            name="/api/v1/products?category"
        )
 
    @task(3)
    def add_to_cart(self):
        payload = {
            "product_id": random.choice(self.inventory),
            "quantity": random.randint(1, 2)
        }
        self.client.post(
            "/api/v1/cart/items",
            json=payload,
            name="/api/v1/cart/items [POST]"
        )
 
    @task(1)
    def view_cart(self):
        self.client.get("/api/v1/cart", name="/api/v1/cart")
 
    @task(1)
    def estimate_shipping(self):
        payload = {
            "postal_code": "10001",
            "country": "US",
            "cart_total": 249.99
        }
        self.client.post(
            "/api/v1/checkout/shipping-estimate",
            json=payload,
            name="/api/v1/checkout/shipping-estimate"
        )
 
    @task(1)
    def price_preview(self):
        payload = {
            "coupon_code": "SPRING10",
            "currency": "USD"
        }
        self.client.post(
            "/api/v1/checkout/price-preview",
            json=payload,
            name="/api/v1/checkout/price-preview"
        )

Why This Scenario Is Valuable

This kind of test is excellent for performance testing in GitHub Actions because it validates more than simple reads:

  • cart writes
  • inventory checks
  • shipping calculations
  • pricing and discount logic

These operations are often database-heavy and are more likely to regress after code changes than static content endpoints.

Pull Request Performance Gate Pattern

A common CI/CD pattern is to run a short, moderate load test on every pull request. The goal is not full-scale stress testing, but fast feedback.

For example:

  • 20 users
  • 2 users/sec spawn rate
  • 3-minute duration
  • fail if p95 latency exceeds 800 ms
  • fail if error rate exceeds 1%

You can use GitHub Actions to trigger the test and then query the result. A simplified polling workflow might look like this:

yaml
name: PR Performance Check
 
on:
  pull_request:
    branches:
      - main
 
jobs:
  performance-regression:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger LoadForge test
        id: trigger
        env:
          LOADFORGE_API_TOKEN: ${{ secrets.LOADFORGE_API_TOKEN }}
          LOADFORGE_TEST_ID: ${{ secrets.LOADFORGE_TEST_ID }}
        run: |
          response=$(curl -s -X POST "https://app.loadforge.com/api/v1/tests/${LOADFORGE_TEST_ID}/start/" \
            -H "Authorization: Token ${LOADFORGE_API_TOKEN}" \
            -H "Content-Type: application/json")
          echo "$response"
 
      - name: Wait for test execution
        run: sleep 60
 
      - name: Check result summary
        env:
          LOADFORGE_API_TOKEN: ${{ secrets.LOADFORGE_API_TOKEN }}
          LOADFORGE_TEST_ID: ${{ secrets.LOADFORGE_TEST_ID }}
        run: |
          curl -s "https://app.loadforge.com/api/v1/tests/${LOADFORGE_TEST_ID}/" \
            -H "Authorization: Token ${LOADFORGE_API_TOKEN}"

In a mature setup, you would parse the JSON response and fail the job automatically if SLA thresholds are violated.

Analyzing Your Results

Once your GitHub Actions workflow triggers a LoadForge test, the next step is understanding what the results actually mean.

Key Metrics to Watch

For CI-driven load testing, focus on these metrics:

  • average response time
  • median response time
  • p95 and p99 latency
  • requests per second
  • error rate
  • failed request count
  • endpoint-specific breakdowns

The p95 latency is especially useful for regression detection. Averages can hide outliers, but p95 shows whether a significant portion of users are experiencing slow responses.

What Good Results Look Like

A healthy CI load test usually shows:

  • stable response times over the full run
  • low error rates
  • no sudden latency spikes after ramp-up
  • consistent throughput
  • no single endpoint dominating failures

LoadForge’s real-time reporting makes this easy to inspect while the test is still running. You can quickly determine whether a deployment introduced a bottleneck or whether performance stayed within expected bounds.

Comparing Runs Over Time

The real power of running load testing in GitHub Actions is consistency. If you run the same test on every merge to main, you build a historical baseline. That helps you answer questions like:

  • Did response time increase after the ORM upgrade?
  • Did the new caching layer improve product detail performance?
  • Did authentication get slower after adding MFA checks?
  • Did checkout pricing become unstable after a rules engine change?

LoadForge’s cloud-based platform is especially useful here because it provides repeatable execution environments and distributed testing capacity, reducing the variability you’d get from self-hosted runners.

Performance Optimization Tips

When your GitHub Actions performance testing reveals problems, these are the most common areas to investigate:

Optimize Slow Database Queries

If cart, search, or account endpoints slow down under load:

  • review query plans
  • add missing indexes
  • reduce N+1 query patterns
  • cache expensive reads

Improve Authentication Efficiency

If login or token refresh endpoints become bottlenecks:

  • cache user/session metadata where appropriate
  • reduce repeated DB lookups
  • optimize JWT signing and validation paths
  • review external identity provider latency

Test Realistic User Journeys

Avoid only load testing /health or a static homepage. Instead, target:

  • login
  • search
  • product detail
  • cart
  • checkout preparation
  • account dashboard

These are the endpoints that matter for actual user experience.

Set Practical CI Thresholds

Don’t make your GitHub Actions pipeline fail because a non-critical endpoint moved from 120 ms to 130 ms. Use thresholds that reflect business impact, such as:

  • p95 under 800 ms for key APIs
  • error rate below 1%
  • login success rate above 99%

Separate Fast CI Tests from Larger Stress Tests

Not every workflow needs full stress testing. A strong strategy is:

  • pull requests: short regression-focused load tests
  • main branch: moderate load tests
  • nightly or weekly: larger stress testing runs

LoadForge supports this well because you can scale tests up or down without changing your CI runner footprint.

Common Pitfalls to Avoid

Running Load From GitHub Runners Directly

GitHub Actions runners are not a substitute for a real load generation platform. They’re great for orchestration, not distributed traffic generation. Use LoadForge’s cloud-based infrastructure for realistic performance testing.

Testing Unstable Environments

If your staging environment is under-provisioned, constantly changing, or shared with unrelated QA work, your results may be noisy and misleading. Try to test against a stable, production-like environment whenever possible.

Using Unrealistic Traffic Patterns

A script that repeatedly calls one endpoint without authentication, think time, or varied payloads won’t tell you much. Use Locust scripts that simulate actual user behavior.

Ignoring Authentication and State

Many real bottlenecks appear only in authenticated or stateful flows. If you only test anonymous GET requests, you may miss issues in sessions, permissions, carts, or account APIs.

No Automated Pass/Fail Criteria

If GitHub Actions triggers a test but nobody checks the result, performance testing becomes theater. Define thresholds and enforce them in your CI/CD pipeline.

Overloading Staging Accidentally

Be careful with user counts and spawn rates. A staging environment often has fewer resources than production, so start small and increase gradually.

Conclusion

Adding load testing to GitHub Actions with LoadForge is one of the most effective ways to catch performance regressions early in your CI/CD process. Instead of discovering slow endpoints, authentication bottlenecks, or checkout failures after deployment, you can detect them automatically during pull requests, merges, and release workflows.

By combining GitHub Actions for orchestration with LoadForge’s Locust-based scripting, distributed testing, global test locations, real-time reporting, and CI/CD integration, you get a practical and scalable performance testing workflow that fits naturally into modern DevOps practices.

If you’re ready to make load testing a standard part of your pipeline, try LoadForge and start building performance checks directly into your GitHub Actions workflows.

Try LoadForge free for 7 days

Set up your first load test in under 2 minutes. No commitment.