
Introduction
Pre-deployment load testing in CI/CD pipelines is one of the most effective ways to catch performance regressions before they reach production. Functional tests can tell you whether an application works, but they rarely tell you whether it still works under pressure. A feature that passes unit and integration tests may still introduce slower database queries, memory spikes, API bottlenecks, or authentication slowdowns that only appear during realistic traffic.
By adding load testing and performance testing to your CI/CD workflow, you can validate that every release candidate meets baseline scalability requirements before deployment. This is especially valuable for teams shipping frequently, where even small performance regressions can compound over time.
LoadForge makes this process practical by combining cloud-based infrastructure, distributed testing, real-time reporting, CI/CD integration, and global test locations with the flexibility of Locust. That means you can define performance gates as code, run pre-deployment stress testing automatically, and fail a pipeline if response times or error rates exceed acceptable thresholds.
In this guide, you’ll learn how to build realistic pre-deployment load tests for CI/CD pipelines using LoadForge and Locust, including authenticated API flows, multi-step user journeys, and release validation scenarios that developers actually use in staging environments.
Prerequisites
Before you start, make sure you have the following:
- A LoadForge account
- A staging, pre-production, or ephemeral environment to test against
- API endpoints that closely mirror production behavior
- Test credentials or service accounts for authentication
- Baseline performance targets, such as:
- 95th percentile response time under 500 ms
- Error rate below 1%
- Login endpoint under 300 ms
- Checkout or order submission under 1 second
- Access to your CI/CD platform, such as GitHub Actions, GitLab CI, Jenkins, Azure DevOps, or CircleCI
- A Locust-based Python test script ready to run in LoadForge
You should also define what “pass” means for pre-deployment load testing. CI/CD load testing is most effective when it acts as a release gate. For example:
- Fail the build if p95 latency increases by more than 20%
- Fail the release if any critical endpoint exceeds a fixed threshold
- Fail deployment if authentication or checkout error rates exceed 0.5%
Without clear thresholds, load testing becomes observational instead of actionable.
Understanding CI/CD & DevOps Under Load
CI/CD systems themselves are not usually the thing being load tested. Instead, the pipeline becomes the automation layer that runs load testing against your application before deployment. In a modern DevOps workflow, this creates a feedback loop where performance testing is treated like any other quality check.
Why pre-deployment load testing matters
Applications often degrade under load in ways that aren’t obvious during normal QA:
- Slow database queries triggered by new code paths
- Increased contention on shared resources
- Authentication bottlenecks from token generation or session validation
- Cache misses after deployment
- API fan-out issues when one endpoint calls multiple downstream services
- File upload or report generation endpoints consuming too much CPU or memory
When these issues are discovered after release, rollback and incident response become expensive. Running load testing in CI/CD helps catch them earlier, when fixes are faster and safer.
Common bottlenecks in pre-release environments
When you run performance testing in a pipeline, expect these common bottlenecks:
- Under-provisioned staging environments
- Shared databases used by multiple QA processes
- Incomplete caching layers compared to production
- Missing CDN behavior
- Lower autoscaling limits
- Test data that does not reflect production cardinality
This means you should interpret results carefully. The goal of pre-deployment load testing is not always to exactly reproduce production traffic. More often, it is to detect regressions relative to previous builds and validate that critical workflows still meet acceptable performance targets.
What to test in a CI/CD pipeline
The best candidates for automated load testing are:
- Login and token refresh flows
- Core API endpoints
- Search or filtering endpoints
- Cart, checkout, or order placement flows
- File upload endpoints
- Background-job-triggering endpoints
- Admin or internal APIs critical to release validation
Keep your pipeline tests focused. A short, targeted test that runs on every deployment is often more valuable than a huge stress test that runs too infrequently.
Writing Your First Load Test
Let’s start with a realistic pre-deployment load test for a staging API. This example simulates authenticated users logging in, browsing products, and viewing a product detail page. These are common release validation checks for an e-commerce or SaaS application.
Basic authenticated smoke load test
from locust import HttpUser, task, between
import os
class StagingApiUser(HttpUser):
wait_time = between(1, 3)
def on_start(self):
self.email = os.getenv("LOADFORGE_TEST_EMAIL", "perf-test-user@example.com")
self.password = os.getenv("LOADFORGE_TEST_PASSWORD", "SuperSecure123!")
self.access_token = None
self.login()
def login(self):
response = self.client.post(
"/api/v1/auth/login",
json={
"email": self.email,
"password": self.password,
"rememberMe": True
},
name="POST /api/v1/auth/login"
)
if response.status_code == 200:
data = response.json()
self.access_token = data.get("access_token")
self.client.headers.update({
"Authorization": f"Bearer {self.access_token}",
"Content-Type": "application/json"
})
@task(3)
def list_products(self):
self.client.get(
"/api/v1/products?category=electronics&page=1&limit=24&sort=popularity",
name="GET /api/v1/products"
)
@task(2)
def view_product_detail(self):
self.client.get(
"/api/v1/products/SKU-104582",
name="GET /api/v1/products/:id"
)
@task(1)
def view_profile(self):
self.client.get(
"/api/v1/users/me",
name="GET /api/v1/users/me"
)What this test does
This script is a good starting point for CI/CD load testing because it:
- Authenticates with a realistic login endpoint
- Uses a bearer token like a real client application
- Exercises critical read-heavy endpoints
- Models weighted traffic patterns
- Is small enough to run quickly in a pipeline
In a pre-deployment scenario, this kind of test can run automatically after your app is deployed to staging but before production approval. If response times suddenly spike after a code change, your pipeline can block the release.
Why this works well in CI/CD
For CI/CD and DevOps teams, short validation tests are ideal because they:
- Finish fast enough for frequent execution
- Catch obvious regressions in critical paths
- Produce comparable metrics across builds
- Can be used as automated release gates
In LoadForge, you can run this script using distributed testing infrastructure and compare results across builds to identify trends over time.
Advanced Load Testing Scenarios
Once you have a basic smoke load test in place, the next step is to model more realistic application behavior. Below are several advanced scenarios that fit well into pre-deployment CI/CD pipelines.
Scenario 1: Multi-step user journey with cart and checkout
This example simulates a more complete transaction flow: login, browse, add to cart, and submit an order. This is valuable for performance testing release candidates where business-critical workflows must remain fast.
from locust import HttpUser, task, between
import os
import random
class CheckoutUser(HttpUser):
wait_time = between(2, 5)
def on_start(self):
self.email = os.getenv("LOADFORGE_TEST_EMAIL", "checkout-user@example.com")
self.password = os.getenv("LOADFORGE_TEST_PASSWORD", "CheckoutPass123!")
self.access_token = None
self.cart_id = None
self.product_ids = ["SKU-104582", "SKU-209871", "SKU-330145"]
self.login()
self.create_cart()
def login(self):
with self.client.post(
"/api/v1/auth/login",
json={
"email": self.email,
"password": self.password
},
name="POST /api/v1/auth/login",
catch_response=True
) as response:
if response.status_code != 200:
response.failure(f"Login failed: {response.status_code}")
return
token = response.json().get("access_token")
if not token:
response.failure("No access token returned")
return
self.access_token = token
self.client.headers.update({
"Authorization": f"Bearer {self.access_token}",
"Content-Type": "application/json"
})
response.success()
def create_cart(self):
with self.client.post(
"/api/v1/carts",
json={"currency": "USD"},
name="POST /api/v1/carts",
catch_response=True
) as response:
if response.status_code == 201:
self.cart_id = response.json().get("id")
response.success()
else:
response.failure(f"Cart creation failed: {response.status_code}")
@task(4)
def browse_catalog(self):
category = random.choice(["electronics", "accessories", "home-office"])
self.client.get(
f"/api/v1/products?category={category}&page=1&limit=20",
name="GET /api/v1/products"
)
@task(3)
def add_item_to_cart(self):
if not self.cart_id:
return
product_id = random.choice(self.product_ids)
self.client.post(
f"/api/v1/carts/{self.cart_id}/items",
json={
"productId": product_id,
"quantity": 1
},
name="POST /api/v1/carts/:id/items"
)
@task(1)
def checkout(self):
if not self.cart_id:
return
self.client.post(
f"/api/v1/orders",
json={
"cartId": self.cart_id,
"paymentMethod": "test-card",
"shippingAddress": {
"firstName": "Perf",
"lastName": "Tester",
"line1": "100 Load Test Ave",
"city": "Austin",
"state": "TX",
"postalCode": "73301",
"country": "US"
}
},
name="POST /api/v1/orders"
)This is a strong fit for stress testing and release validation because it includes stateful interactions. If a deployment introduces slower cart writes, order processing delays, or session issues, this script will expose them.
Scenario 2: Token refresh and protected API usage
Many modern applications use short-lived JWTs with refresh tokens. If your release changes authentication middleware, token signing, or session storage, load testing this flow in CI/CD is essential.
from locust import HttpUser, task, between
import os
import time
class TokenRefreshUser(HttpUser):
wait_time = between(1, 2)
def on_start(self):
self.email = os.getenv("LOADFORGE_TEST_EMAIL", "api-user@example.com")
self.password = os.getenv("LOADFORGE_TEST_PASSWORD", "ApiPass123!")
self.access_token = None
self.refresh_token = None
self.token_acquired_at = 0
self.authenticate()
def authenticate(self):
with self.client.post(
"/oauth/token",
data={
"grant_type": "password",
"username": self.email,
"password": self.password,
"client_id": "staging-web-client",
"client_secret": "staging-client-secret"
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
name="POST /oauth/token",
catch_response=True
) as response:
if response.status_code != 200:
response.failure(f"Authentication failed: {response.status_code}")
return
payload = response.json()
self.access_token = payload.get("access_token")
self.refresh_token = payload.get("refresh_token")
self.token_acquired_at = time.time()
self.client.headers.update({
"Authorization": f"Bearer {self.access_token}"
})
response.success()
def refresh_access_token(self):
with self.client.post(
"/oauth/token",
data={
"grant_type": "refresh_token",
"refresh_token": self.refresh_token,
"client_id": "staging-web-client",
"client_secret": "staging-client-secret"
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
name="POST /oauth/token [refresh]",
catch_response=True
) as response:
if response.status_code != 200:
response.failure(f"Token refresh failed: {response.status_code}")
return
payload = response.json()
self.access_token = payload.get("access_token")
self.refresh_token = payload.get("refresh_token", self.refresh_token)
self.token_acquired_at = time.time()
self.client.headers.update({
"Authorization": f"Bearer {self.access_token}"
})
response.success()
@task(5)
def get_dashboard_data(self):
if time.time() - self.token_acquired_at > 240:
self.refresh_access_token()
self.client.get(
"/api/v2/dashboard/summary?range=7d",
name="GET /api/v2/dashboard/summary"
)
@task(2)
def get_usage_report(self):
if time.time() - self.token_acquired_at > 240:
self.refresh_access_token()
self.client.get(
"/api/v2/reports/usage?teamId=team_4821&period=current_month",
name="GET /api/v2/reports/usage"
)This script is especially useful for CI/CD & DevOps teams working on API gateways, auth services, or middleware changes. Authentication regressions often show up under concurrency before they show up in manual testing.
Scenario 3: File upload and asynchronous processing validation
If your application supports imports, media uploads, or document processing, you should validate those endpoints before deployment. These workflows often stress CPU, memory, object storage, and background job systems.
from locust import HttpUser, task, between
import os
import json
from io import BytesIO
class FileUploadUser(HttpUser):
wait_time = between(3, 6)
def on_start(self):
self.api_key = os.getenv("LOADFORGE_API_KEY", "staging-api-key")
self.client.headers.update({
"X-API-Key": self.api_key
})
@task(2)
def upload_customer_import(self):
csv_content = (
"email,first_name,last_name,plan\n"
"alice@example.com,Alice,Nguyen,pro\n"
"bob@example.com,Bob,Smith,business\n"
"carol@example.com,Carol,Jones,enterprise\n"
)
files = {
"file": ("customers.csv", BytesIO(csv_content.encode("utf-8")), "text/csv")
}
data = {
"importType": "customer_sync",
"notifyOnCompletion": "true"
}
with self.client.post(
"/api/v1/imports/customers",
files=files,
data=data,
name="POST /api/v1/imports/customers",
catch_response=True
) as response:
if response.status_code not in (200, 202):
response.failure(f"Upload failed: {response.status_code}")
return
job_id = response.json().get("jobId")
if not job_id:
response.failure("No jobId returned from upload")
return
response.success()
self.check_import_status(job_id)
def check_import_status(self, job_id):
self.client.get(
f"/api/v1/imports/{job_id}/status",
name="GET /api/v1/imports/:jobId/status"
)
@task(1)
def list_recent_imports(self):
self.client.get(
"/api/v1/imports?limit=10&status=completed",
name="GET /api/v1/imports"
)This scenario is useful when validating releases that affect upload handlers, background workers, or queue systems. It can also reveal whether a deployment causes spikes in processing latency or increased 5xx errors on ingestion endpoints.
Analyzing Your Results
Running the test is only half the work. The real value of pre-deployment load testing comes from interpreting results correctly and using them to make release decisions.
Key metrics to watch
For CI/CD load testing, focus on these metrics:
- Response time percentiles, especially p95 and p99
- Error rate
- Requests per second
- Authentication success/failure rates
- Throughput across critical endpoints
- Endpoint-specific latency trends
- Time to first failure under load
Average response time can be misleading. Averages hide spikes. For release gates, p95 and p99 latency are usually more meaningful.
What a failed pre-deployment test might indicate
If your load test fails in the pipeline, common causes include:
- A new database migration causing slower queries
- Missing indexes
- Increased payload size from a new feature
- Authentication middleware regression
- N+1 query issues
- Cache invalidation after deployment
- Thread pool or connection pool exhaustion
- Increased reliance on a slow downstream service
Using LoadForge to compare builds
LoadForge is particularly helpful here because it provides real-time reporting and makes it easier to compare test runs over time. That matters in CI/CD because you often care less about absolute numbers and more about whether the latest build is worse than the previous one.
Use LoadForge to:
- Track latency trends across releases
- Spot endpoint-level regressions quickly
- Run distributed testing from cloud infrastructure
- Execute tests from global test locations if geographic behavior matters
- Integrate performance testing into deployment pipelines
A practical approach is to establish a baseline from a known-good build, then compare every new release candidate against that baseline.
Example pipeline gating strategy
A simple release policy might be:
- Fail if p95 for login exceeds 400 ms
- Fail if p95 for checkout exceeds 1,200 ms
- Fail if total error rate exceeds 1%
- Warn if throughput drops by more than 15%
This turns performance testing into a measurable DevOps control rather than a manual review step.
Performance Optimization Tips
When pre-deployment load testing reveals issues, these optimization strategies often help:
Optimize critical database paths
Look for:
- Slow joins
- Missing indexes
- Repeated queries in loops
- Large result sets without pagination
The endpoints you test in CI/CD should map to your most important database-backed flows.
Cache expensive reads
If product lists, dashboards, or reports are slow under load, caching can dramatically improve performance. Validate both cold-cache and warm-cache behavior in staging.
Reduce authentication overhead
Authentication often becomes a bottleneck under concurrency. Consider:
- More efficient token validation
- Caching public keys or auth metadata
- Reducing unnecessary session lookups
- Tuning identity provider integrations
Tune connection pools and worker settings
If your app depends on databases, queues, or external APIs, check pool sizes and timeout settings. Many pre-deployment issues come from saturation rather than raw CPU limits.
Test realistic payloads
Tiny synthetic payloads can hide real bottlenecks. Use realistic JSON bodies, query parameters, file sizes, and auth flows so your load testing reflects production behavior.
Keep CI/CD tests focused
For pipeline speed, use a layered strategy:
- Short smoke load test on every pull request or merge
- Medium pre-deployment validation before release
- Larger stress testing or soak testing on a schedule
This gives you fast feedback without slowing delivery too much.
Common Pitfalls to Avoid
Testing with unrealistic traffic patterns
If every virtual user only hits the homepage or a single GET endpoint, your results won’t reflect real application behavior. Model actual user flows.
Ignoring authentication flows
Many teams test only public endpoints and miss bottlenecks in login, token refresh, or session validation. In real systems, auth is often a major part of performance.
Running tests against unstable shared environments
If your staging environment is being used by QA, data migrations, and integration tests at the same time, your results may be noisy. Try to isolate pre-deployment performance testing as much as possible.
Using only average response time
Averages can look fine even when a meaningful percentage of users experience severe latency. Always review p95, p99, and error rates.
Making tests too large for the pipeline
A 90-minute stress test should not block every deployment. Keep your CI/CD load testing practical and fast, then run deeper stress testing separately.
Forgetting cleanup and test data management
Cart creation, order placement, uploads, and imports generate data. If your scripts don’t account for test data lifecycle, results can drift over time or environments can become polluted.
Not versioning performance tests
Your load testing scripts should evolve with your application. Store them in version control, review them like code, and update them whenever critical workflows change.
Conclusion
Pre-deployment load testing in CI/CD pipelines helps teams catch scalability issues before release, reduce production risk, and make performance testing part of everyday engineering practice. By combining realistic Locust scripts with automated pipeline execution, you can validate login flows, API performance, checkout journeys, file uploads, and other critical paths before a deployment is approved.
LoadForge makes this easier with cloud-based infrastructure, distributed testing, real-time reporting, CI/CD integration, and global test locations. Whether you want lightweight release gates or more advanced pre-production stress testing, it gives your team a practical way to build performance checks directly into your DevOps workflow.
If you’re ready to prevent performance regressions before they reach production, try LoadForge and start adding pre-deployment load testing to your CI/CD pipeline today.
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.