name: performance-testing description: Load testing with k6/Artillery, response time thresholds, memory leak detection, N+1 query detection, and CI integration.
Performance Testing
k6 Script Patterns
Basic scenario with stages
// k6 run load-test.js
import http from 'k6/http'
import { check, sleep } from 'k6'
export const options = {
stages: [
{ duration: '30s', target: 10 }, // ramp up
{ duration: '1m', target: 50 }, // hold load
{ duration: '30s', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(95)<200', 'p(99)<500'],
http_req_failed: ['rate<0.01'], // < 1% error rate
},
}
export default function () {
const res = http.get('https://api.example.com/users')
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200,
})
sleep(1)
}
POST with auth
export default function () {
const payload = JSON.stringify({ email: 'test@example.com', password: 'secret' })
const headers = { 'Content-Type': 'application/json' }
const res = http.post(`${BASE_URL}/auth/login`, payload, { headers })
const token = res.json('token')
http.get(`${BASE_URL}/profile`, {
headers: { Authorization: `Bearer ${token}` },
})
}
Load Test Types
| Type | Duration | Target VU | Purpose |
|---|---|---|---|
| Smoke | 1 min | 1-5 | Verify script works, no regressions |
| Load | 30 min | expected peak | Normal production conditions |
| Stress | 60 min | 2-3x peak | Find breaking point |
| Spike | 2 min | 10x peak → 0 | Sudden traffic burst behavior |
| Soak | 4-8 hours | 80% peak | Memory leaks, degradation over time |
Threshold Definitions
export const options = {
thresholds: {
// Response time
http_req_duration: ['p(95)<200', 'p(99)<500', 'avg<100'],
// Error rate
http_req_failed: ['rate<0.01'], // < 1%
// Custom metric for specific endpoint
'http_req_duration{name:login}': ['p(95)<300'],
// Checks pass rate
checks: ['rate>0.99'],
},
}
CI Integration (GitHub Actions + k6)
# .github/workflows/perf.yml
name: Performance Tests
on:
pull_request:
branches: [main]
jobs:
k6:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run k6 smoke test
uses: grafana/k6-action@v0.3.1
with:
filename: tests/perf/smoke.js
flags: --out json=results.json
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: k6-results
path: results.json
Memory Leak Detection (Node.js)
Heap snapshot approach
# Start with --inspect
node --inspect --expose-gc server.js
# In Chrome DevTools → Memory → Take heap snapshot
# Run load, take another snapshot
# Compare: growing retained objects = leak
Programmatic detection
import v8 from 'v8'
function checkHeap(label) {
const stats = v8.getHeapStatistics()
console.log(`[${label}] Heap used: ${Math.round(stats.used_heap_size / 1024 / 1024)}MB`)
}
setInterval(() => checkHeap('monitor'), 30_000)
Common leak patterns to watch
// BAD: event listener never removed
emitter.on('data', handler) // grows on every request
// GOOD: cleanup in teardown
emitter.on('data', handler)
return () => emitter.off('data', handler)
// BAD: unbounded cache
const cache = {}
cache[userId] = data // never evicted
// GOOD: bounded cache
import LRU from 'lru-cache'
const cache = new LRU({ max: 1000, ttl: 1000 * 60 * 5 })
N+1 Query Detection
pg_stat_statements (PostgreSQL)
-- Enable extension
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Find repetitive queries during a load test window
SELECT
query,
calls,
mean_exec_time,
total_exec_time
FROM pg_stat_statements
WHERE calls > 100
ORDER BY calls DESC
LIMIT 20;
Query logging (development)
// Prisma: log all queries
const prisma = new PrismaClient({
log: ['query'],
})
// Detect N+1: same query fired N times in a request
// Fix: use include/select or DataLoader
DataLoader pattern (N+1 fix)
import DataLoader from 'dataloader'
const userLoader = new DataLoader(async (ids) => {
const users = await db.user.findMany({ where: { id: { in: ids } } })
return ids.map(id => users.find(u => u.id === id))
})
// In resolver — batches automatically
const user = await userLoader.load(post.authorId)
Web Vitals / Lighthouse CI
# .github/workflows/lhci.yml
- name: Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
// lighthouserc.json
{
"ci": {
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.8 }],
"first-contentful-paint": ["error", { "maxNumericValue": 2000 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }]
}
}
}
}
Trend Tracking
Store k6 results to Grafana/InfluxDB for trend visualization:
k6 run --out influxdb=http://localhost:8086/k6 load-test.js
Or export JSON and compare baselines:
k6 run --out json=results-$(git rev-parse --short HEAD).json load-test.js