
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_TOKENLOADFORGE_TEST_IDSTAGING_BASE_URLTEST_USERNAMETEST_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
/healthonly - 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:
- Deploy or identify the target environment
- Trigger a LoadForge test
- Wait for the results
- Evaluate pass/fail criteria
- 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
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.
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.
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.
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.
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:
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.
LoadForge Team
LoadForge is a load and performance testing platform built on Locust. Our team has been shipping load tests against production systems since 2018, and we write these guides from real customer engagements.
Related guides
Keep going with more guides from the same category.

ArgoCD Load Testing for Progressive Delivery
Combine ArgoCD and LoadForge to validate app performance during progressive delivery and Kubernetes rollouts.

How to Automate Load Testing in CircleCI
Use LoadForge with CircleCI to automate load testing in CI/CD and detect bottlenecks before production.

Datadog Load Testing Integration with LoadForge
Integrate Datadog with LoadForge to correlate load test results with infrastructure and application metrics.