Part of our Performance & Scalability series
Read the complete guideLoad Testing Strategies for Web Applications: Find Breaking Points Before Users Do
80% of performance issues are discovered by end users, not by testing. Load testing flips this ratio by simulating real-world traffic patterns against your application before users encounter problems. The difference between a site that handles Black Friday traffic and one that crashes is almost always whether someone ran a load test first.
This guide covers load testing methodology, tool selection, test design, and result interpretation for web applications, eCommerce platforms, and ERP systems.
Key Takeaways
- Load testing should simulate realistic user behavior, not just hammer a single endpoint
- Establish performance baselines before optimizing --- you cannot improve what you have not measured
- Run load tests in a production-like environment; staging results may not reflect production behavior
- Automate load tests in CI/CD to catch performance regressions before deployment
Types of Load Tests
| Test Type | Purpose | Duration | Load Pattern |
|---|---|---|---|
| Smoke test | Verify basic functionality under minimal load | 1-2 minutes | 1-5 users |
| Load test | Validate performance under expected traffic | 10-30 minutes | Normal traffic |
| Stress test | Find the breaking point | 15-30 minutes | Gradually increasing |
| Spike test | Test sudden traffic surges | 5-10 minutes | Sudden jump |
| Soak test | Detect memory leaks and degradation | 2-8 hours | Sustained normal load |
Load Testing with k6
Basic Load Test
// k6/load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 50 }, // Ramp up to 50 users
{ duration: '5m', target: 50 }, // Stay at 50 users
{ duration: '2m', target: 100 }, // Ramp up to 100 users
{ duration: '5m', target: 100 }, // Stay at 100 users
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.01'],
http_reqs: ['rate>100'],
},
};
export default function () {
// Simulate realistic user behavior
const homeResponse = http.get('https://example.com/');
check(homeResponse, {
'homepage status is 200': (r) => r.status === 200,
'homepage loads in under 1s': (r) => r.timings.duration < 1000,
});
sleep(Math.random() * 3 + 1); // 1-4 seconds think time
const productsResponse = http.get('https://example.com/api/v1/products');
check(productsResponse, {
'products API is 200': (r) => r.status === 200,
'products API under 500ms': (r) => r.timings.duration < 500,
});
sleep(Math.random() * 2 + 1);
}
eCommerce User Journey Test
// k6/ecommerce-journey.js
import http from 'k6/http';
import { check, group, sleep } from 'k6';
export const options = {
scenarios: {
browsing: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '5m', target: 200 },
{ duration: '10m', target: 200 },
{ duration: '5m', target: 0 },
],
exec: 'browsingScenario',
},
purchasing: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '5m', target: 20 },
{ duration: '10m', target: 20 },
{ duration: '5m', target: 0 },
],
exec: 'purchaseScenario',
},
},
thresholds: {
'http_req_duration{scenario:browsing}': ['p(95)<800'],
'http_req_duration{scenario:purchasing}': ['p(95)<2000'],
http_req_failed: ['rate<0.01'],
},
};
export function browsingScenario() {
group('Browse Products', () => {
http.get('https://store.example.com/');
sleep(2);
http.get('https://store.example.com/products');
sleep(3);
http.get('https://store.example.com/products/sample-product');
sleep(2);
});
}
export function purchaseScenario() {
group('Purchase Flow', () => {
// Browse
http.get('https://store.example.com/products/sample-product');
sleep(1);
// Add to cart
http.post('https://store.example.com/api/cart', JSON.stringify({
productId: 'prod_123',
quantity: 1,
}), { headers: { 'Content-Type': 'application/json' } });
sleep(2);
// Checkout
http.get('https://store.example.com/cart');
sleep(3);
// Place order (simulated)
const orderResponse = http.post('https://store.example.com/api/checkout/validate', JSON.stringify({
email: `test-${__VU}@example.com`,
}), { headers: { 'Content-Type': 'application/json' } });
check(orderResponse, {
'checkout validates': (r) => r.status === 200 || r.status === 201,
});
sleep(1);
});
}
Stress Test (Find the Breaking Point)
// k6/stress-test.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 200 },
{ duration: '5m', target: 200 },
{ duration: '2m', target: 500 },
{ duration: '5m', target: 500 },
{ duration: '2m', target: 1000 },
{ duration: '5m', target: 1000 },
{ duration: '5m', target: 0 },
],
};
export default function () {
const res = http.get('https://example.com/api/v1/products');
check(res, {
'status is 200': (r) => r.status === 200,
});
}
Interpreting Results
Key Metrics
| Metric | Healthy | Warning | Critical |
|---|---|---|---|
| P95 Response Time | <500ms | 500ms-2s | >2s |
| P99 Response Time | <1s | 1-5s | >5s |
| Error Rate | <0.1% | 0.1-1% | >1% |
| Throughput | Meets target | 80% of target | <80% of target |
Common Bottleneck Patterns
CPU-bound bottleneck: Response time increases linearly with load. P95 and P99 diverge slowly.
- Fix: Optimize hot code paths, add CPU capacity, or scale horizontally
Database bottleneck: Response time increases exponentially at a specific load threshold. Connection pool exhaustion.
- Fix: Query optimization, connection pooling, read replicas (see database scaling guide)
Memory bottleneck: Gradual degradation over time. GC pauses cause latency spikes.
- Fix: Increase memory, fix memory leaks, optimize object allocation
Network bottleneck: Response time increases uniformly across all endpoints. Bandwidth saturation.
- Fix: CDN for static assets, compression, reduce payload sizes
Performance Baselines
Establishing a Baseline
Before optimizing, document your current performance:
# Run baseline test
k6 run --out json=baseline-results.json k6/load-test.js
# Compare after optimization
k6 run --out json=optimized-results.json k6/load-test.js
Performance Budget
Define acceptable performance for each endpoint:
| Endpoint | P95 Target | Throughput Target |
|---|---|---|
| Homepage | 500ms | 200 req/s |
| Product listing | 800ms | 150 req/s |
| Product detail | 600ms | 200 req/s |
| Add to cart | 300ms | 100 req/s |
| Checkout | 2000ms | 50 req/s |
| Search | 500ms | 100 req/s |
| Admin dashboard | 1500ms | 20 req/s |
CI/CD Integration
Automated Performance Regression Testing
# .github/workflows/performance.yml
name: Performance Test
on:
push:
branches: [main]
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run k6 load test
uses: grafana/[email protected]
with:
filename: k6/load-test.js
flags: --out json=results.json
env:
K6_TARGET_URL: ${{ secrets.STAGING_URL }}
- name: Check thresholds
run: |
if grep -q '"thresholds":{".*":"fail"' results.json; then
echo "Performance thresholds exceeded!"
exit 1
fi
Load Testing Checklist
Before Testing
- Environment matches production (instance types, database size)
- Test data is representative (realistic product count, user count)
- Monitoring is active (track server metrics during test)
- Stakeholders are notified (load tests can trigger alerts)
- CDN and caching are configured as in production
During Testing
- Monitor server CPU, memory, disk I/O
- Monitor database connections and query latency
- Watch for error rate increases
- Check for resource exhaustion (file descriptors, connections)
- Note the load level where performance degrades
After Testing
- Document baseline results
- Identify bottlenecks and their load thresholds
- Create tickets for performance improvements
- Compare against performance budget
- Schedule follow-up test after optimizations
Frequently Asked Questions
Should we load test production or staging?
Both, if possible. Staging for regular testing and CI/CD integration. Production for periodic validation (during low-traffic hours) because staging often differs in database size, cache warmth, CDN configuration, and network topology. If you can only test one environment, test staging but make it as production-like as possible.
How often should we run load tests?
Smoke tests on every deployment (automated in CI/CD). Full load tests weekly or before major releases. Stress tests quarterly or before known high-traffic events (sales, launches). Soak tests quarterly to detect memory leaks and long-term degradation.
How do we load test an ERP system?
ERP load testing requires simulating concurrent users performing different tasks: generating invoices, creating purchase orders, running reports, importing data. Focus on the heaviest operations (report generation, data imports) and the most concurrent operations (order entry during peak hours). ECOSIRE provides Odoo performance testing as part of our support services.
What is a realistic think time between requests?
For eCommerce browsing: 2-5 seconds. For form filling: 10-30 seconds. For checkout: 15-60 seconds. For admin/ERP usage: 5-15 seconds. Always add randomized think time to your load tests --- constant intervals create unrealistic synchronized load patterns.
What Comes Next
Load testing reveals bottlenecks that guide your optimization efforts. Follow up with database scaling for database bottlenecks, CDN optimization for static asset delivery, and auto-scaling for elastic capacity.
Contact ECOSIRE for performance testing and optimization, or explore our DevOps guide for the complete infrastructure strategy.
Published by ECOSIRE -- helping businesses build applications that perform under pressure.
Written by
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Grow Your Business with ECOSIRE
Enterprise solutions across ERP, eCommerce, AI, analytics, and automation.
Related Articles
GitHub Actions CI/CD for Monorepo Projects
Complete GitHub Actions CI/CD guide for Turborepo monorepos: affected-only builds, parallel jobs, caching strategies, environment-based deploys, and security best practices.
k6 Load Testing: Stress-Test Your APIs Before Launch
Master k6 load testing for Node.js APIs. Covers virtual user ramp-ups, thresholds, scenarios, HTTP/2, WebSocket testing, Grafana dashboards, and CI integration patterns.
Odoo Performance Tuning: PostgreSQL and Server Optimization
Expert guide to Odoo 19 performance tuning. Covers PostgreSQL configuration, indexing, query optimization, Nginx caching, and server sizing for enterprise deployments.
More from Performance & Scalability
Webhook Debugging and Monitoring: The Complete Troubleshooting Guide
Master webhook debugging with this complete guide covering failure patterns, debugging tools, retry strategies, monitoring dashboards, and security best practices.
k6 Load Testing: Stress-Test Your APIs Before Launch
Master k6 load testing for Node.js APIs. Covers virtual user ramp-ups, thresholds, scenarios, HTTP/2, WebSocket testing, Grafana dashboards, and CI integration patterns.
Nginx Production Configuration: SSL, Caching, and Security
Nginx production configuration guide: SSL termination, HTTP/2, caching headers, security headers, rate limiting, reverse proxy setup, and Cloudflare integration patterns.
Odoo Performance Tuning: PostgreSQL and Server Optimization
Expert guide to Odoo 19 performance tuning. Covers PostgreSQL configuration, indexing, query optimization, Nginx caching, and server sizing for enterprise deployments.
Odoo vs Acumatica: Cloud ERP for Growing Businesses
Odoo vs Acumatica compared for 2026: unique pricing models, scalability, manufacturing depth, and which cloud ERP fits your growth trajectory.
Testing and Monitoring AI Agents in Production
A complete guide to testing and monitoring AI agents in production environments. Covers evaluation frameworks, observability, drift detection, and incident response for OpenClaw deployments.