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 Research and Development Team
Building enterprise-grade digital products at ECOSIRE. Sharing insights on Odoo integrations, e-commerce automation, and AI-powered business solutions.
Related Articles
AI Agent Performance Optimization: Speed, Accuracy, and Cost Efficiency
Optimize AI agent performance across response time, accuracy, and cost with proven techniques for prompt engineering, caching, model selection, and monitoring.
API Gateway Patterns and Best Practices for Modern Applications
Implement API gateway patterns including rate limiting, authentication, request routing, circuit breakers, and API versioning for scalable web architectures.
CDN Performance Optimization: The Complete Guide to Faster Global Delivery
Optimize CDN performance with caching strategies, edge computing, image optimization, and multi-CDN architectures for faster global content delivery.
More from Performance & Scalability
AI Agent Performance Optimization: Speed, Accuracy, and Cost Efficiency
Optimize AI agent performance across response time, accuracy, and cost with proven techniques for prompt engineering, caching, model selection, and monitoring.
Testing and Monitoring AI Agents: Reliability Engineering for Autonomous Systems
Complete guide to testing and monitoring AI agents covering unit testing, integration testing, behavioral testing, observability, and production monitoring strategies.
CDN Performance Optimization: The Complete Guide to Faster Global Delivery
Optimize CDN performance with caching strategies, edge computing, image optimization, and multi-CDN architectures for faster global content delivery.
Mobile SEO for eCommerce: Complete Optimization Guide for 2026
Mobile SEO guide for eCommerce sites. Covers mobile-first indexing, Core Web Vitals, structured data, page speed optimization, and mobile search ranking factors.
Production Monitoring and Alerting: The Complete Setup Guide
Set up production monitoring and alerting with Prometheus, Grafana, and Sentry. Covers metrics, logs, traces, alert policies, and incident response workflows.
API Performance: Rate Limiting, Pagination & Async Processing
Build high-performance APIs with rate limiting algorithms, cursor-based pagination, async job queues, and response compression best practices.