|
1 | | -// tests/load/k6_ramp.js |
| 1 | +/** |
| 2 | + * k6 load test — ramping VUs from 100 to 5000 RPS |
| 3 | + * |
| 4 | + * Usage: |
| 5 | + * k6 run tests/load/k6_ramp.js |
| 6 | + * k6 run tests/load/k6_ramp.js --out json=tests/load/benchmarks/results.json |
| 7 | + * |
| 8 | + * Environment variables: |
| 9 | + * SERVICE_URL — base URL of the rate-limiter service (default: http://localhost:8080) |
| 10 | + * API_KEY_COUNT — number of distinct API keys to rotate across (default: 10) |
| 11 | + * |
| 12 | + * After the run, copy the headline numbers into tests/load/benchmarks/results.md. |
| 13 | + * Those numbers become your CV bullet metrics — only report what you measured. |
| 14 | + */ |
| 15 | + |
| 16 | +import http from 'k6/http'; |
| 17 | +import { check, sleep } from 'k6'; |
| 18 | +import { Rate, Trend, Counter } from 'k6/metrics'; |
| 19 | + |
| 20 | +const rejectionRate = new Rate('rejection_rate'); |
| 21 | +const decisionLatency = new Trend('decision_latency_ms', true); |
| 22 | +const admittedRequests = new Counter('admitted_requests'); |
| 23 | +const rejectedRequests = new Counter('rejected_requests'); |
| 24 | + |
| 25 | +export const options = { |
| 26 | + stages: [ |
| 27 | + { duration: '30s', target: 50 }, // warm-up |
| 28 | + { duration: '60s', target: 200 }, // ramp |
| 29 | + { duration: '60s', target: 500 }, // sustained mid |
| 30 | + { duration: '60s', target: 1000 }, // sustained target |
| 31 | + { duration: '60s', target: 2000 }, // push beyond |
| 32 | + { duration: '30s', target: 5000 }, // peak stress |
| 33 | + { duration: '30s', target: 0 }, // cool down |
| 34 | + ], |
| 35 | + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'], |
| 36 | + thresholds: { |
| 37 | + // p99 decision latency must stay under 10ms at sustained load |
| 38 | + 'http_req_duration{status:200}': ['p(99)<10'], |
| 39 | + // Non-429 errors should be negligible |
| 40 | + 'http_req_failed{status:!429}': ['rate<0.005'], |
| 41 | + }, |
| 42 | +}; |
| 43 | + |
| 44 | +const BASE_URL = __ENV.SERVICE_URL || 'http://localhost:8080'; |
| 45 | +const API_KEY_COUNT = parseInt(__ENV.API_KEY_COUNT || '10', 10); |
| 46 | + |
| 47 | +export default function () { |
| 48 | + const apiKey = `load-test-key-${Math.floor(Math.random() * API_KEY_COUNT)}`; |
| 49 | + const start = Date.now(); |
| 50 | + |
| 51 | + const res = http.post(`${BASE_URL}/check`, null, { |
| 52 | + headers: { 'X-API-Key': apiKey }, |
| 53 | + tags: { api_key: apiKey }, |
| 54 | + }); |
| 55 | + |
| 56 | + decisionLatency.add(Math.max(0, Date.now() - start)); |
| 57 | + |
| 58 | + const isRejected = res.status === 429; |
| 59 | + rejectionRate.add(isRejected); |
| 60 | + |
| 61 | + if (isRejected) { |
| 62 | + rejectedRequests.add(1); |
| 63 | + } else { |
| 64 | + admittedRequests.add(1); |
| 65 | + } |
| 66 | + |
| 67 | + check(res, { |
| 68 | + 'status is 200 or 429': (r) => r.status === 200 || r.status === 429, |
| 69 | + 'response has body': (r) => r.body && r.body.length > 0, |
| 70 | + }); |
| 71 | + sleep(Math.random() * 2); |
| 72 | +} |
| 73 | + |
| 74 | +export function handleSummary(data) { |
| 75 | + return { |
| 76 | + 'tests/load/benchmarks/summary.json': JSON.stringify(data, null, 2), |
| 77 | + }; |
| 78 | +} |
0 commit comments