Part of our Performance & Scalability series
Read the complete guide80% 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
How Much Does Cloud Hosting Cost in 2026? Real Price Breakdown (AWS, Hetzner, DigitalOcean, Odoo.sh)
Real 2026 cloud hosting costs from a team that pays the bills: $5-$25/mo hobby, $50-$400/mo SMB, hidden egress and backup fees, reserved-instance math.
Odoo Hosting Requirements in 2026: Server Sizing by User Count (With Real Configs)
Odoo hosting requirements by user count: vCPU, RAM, storage, and worker settings for 5 to 250+ users, plus PostgreSQL tuning values from real deployments.
Shopify Speed Optimization: A Technical Checklist That Actually Moves Core Web Vitals (2026)
A field-tested Shopify speed checklist for 2026 — what actually improves LCP, INP, and CLS on real stores, what wastes time, and how to audit apps and themes.
More from Performance & Scalability
Shopify Speed Optimization: A Technical Checklist That Actually Moves Core Web Vitals (2026)
A field-tested Shopify speed checklist for 2026 — what actually improves LCP, INP, and CLS on real stores, what wastes time, and how to audit apps and themes.
Technical SEO Audit Checklist 2026: 47 Checks We Run on Every Client Site
The 47-point technical SEO audit checklist we run on every client site in 2026 — crawlability, indexation, canonicals, hreflang, Core Web Vitals, and logs.
Odoo 19 HR: Skills Matrix, Career Plans, Performance Cycles
Odoo 19 HR upgrade: native skills matrix, career path planning, performance review cycles, 9-box grid, succession planning, HRIS integration.
Odoo 19 Performance Benchmarks: PostgreSQL 17 Tuning Numbers
Real-world Odoo 19 performance benchmarks: web client speed, ORM throughput, PG17 tuning settings, connection pooling, worker counts, scaling thresholds.
OpenClaw Cost Optimization and Token Efficiency at Scale
OpenClaw token cost optimization: prompt caching, model routing, response caching, batch APIs, and per-tenant cost guardrails for production agents.
Power BI Incremental Refresh for Tables Over 10 Million Rows
Power BI Incremental Refresh playbook for 10M+ row tables: partition design, RangeStart/RangeEnd, refresh policies, query folding, and DirectQuery hybrids.