Load testing
Find out how much traffic your app takes before a launch, by running k6 against a preview with a slow ramp and hard stop conditions.
runlot health estimates a ceiling from the traffic your app already serves. A load test measures the real curve instead: your code, your queries and your data, with the request rate rising until latency or errors move. Run it before a launch or a campaign, against a preview, never against production.
This page uses k6, a free load-testing tool you run on your own machine.
Rules for the run
- A preview only. Deploy the same code with
runlot deploy --preview <name>. The preview gets its own copy of the database, so the test's writes stay there. Never use--db shared: the test would write to production's database. - Start at 1 request a second and ramp slowly. A jump to full load tells you only that it failed, not where.
- Stop when more than 1% of requests fail, or when p95 goes above 2 seconds. Past that point you are measuring a broken app, and every extra request costs money. The script below stops on its own.
- 10 minutes at most.
- Watch while it runs:
runlot healthand the Infrastructure chart (memory against the limit, CPU) in the dashboard. - It costs budget. A preview's requests and CPU time count against your organization's budget, the same as production's. If the budget runs out, production gets
429 budget_exhaustedtoo. The script below sends about 10,000 requests, a tenth of the free plan's daily allowance. - Run it outside production's busy hours.
1. Deploy a preview
runlot deploy --preview loadThe address is printed when the deploy finishes, for example https://load--my-app.me.runlot.app. If the project is not public, the preview is not either; create a bypass secret with runlot access bypass --new and pass it to k6 (see Deployment access control).
If your app needs data to answer realistic requests, the preview's database is copied from the project's newest backup. Point the test at pages and API routes that read that data, the way real users would.
2. The script
import http from "k6/http";
import { check } from "k6";
const BASE = __ENV.BASE_URL; // https://load--my-app.me.runlot.app
const BYPASS = __ENV.ACCESS_BYPASS; // only for a private project
export const options = {
scenarios: {
ramp: {
// A request rate, not a number of users: the rate is what the app feels.
executor: "ramping-arrival-rate",
startRate: 1,
timeUnit: "1s",
preAllocatedVUs: 20,
maxVUs: 200,
stages: [
{ target: 5, duration: "2m" },
{ target: 20, duration: "3m" },
{ target: 50, duration: "3m" },
{ target: 0, duration: "1m" },
],
},
},
thresholds: {
// Stop the whole run when more than 1% of requests fail, or when p95 goes above 2 s.
http_req_failed: [{ threshold: "rate<0.01", abortOnFail: true, delayAbortEval: "30s" }],
http_req_duration: [{ threshold: "p(95)<2000", abortOnFail: true, delayAbortEval: "30s" }],
},
};
export default function () {
const headers = BYPASS ? { "Runlot-Access-Bypass": BYPASS } : {};
// Replace with the routes your users hit most.
const res = http.get(`${BASE}/`, { headers });
check(res, { "status is 2xx": (r) => r.status >= 200 && r.status < 300 });
}The ramp runs for 9 minutes and stops at 50 requests a second. Change the target values to fit what you expect, and keep the total under 10 minutes.
3. Run it
k6 run -e BASE_URL=https://load--my-app.me.runlot.app load.jsWhile it runs, keep the preview's Infrastructure chart open and run runlot health in another terminal. Stop it with Ctrl-C if memory approaches the limit; k6 prints its summary either way.
4. Read the result
- The knee: the request rate at which p95 latency starts to climb or the first errors appear. That is the app's real ceiling for these routes. Compare it with the ceiling
runlot healthestimates. - Memory: if memory climbs with the request rate and does not come back down after the run, it will reach the limit under real traffic too. Fix that before the launch.
- Errors:
429 budget_exhaustedmeans the organization's budget ran out, not that the app failed. A500with the invocation outcomeexceededCpumeans one request used 60 seconds of CPU. - If k6 stopped on a threshold, the last stage it reached is above what the app takes.
5. Clean up
runlot preview rm loadThis deletes the preview and its database copy. If you created a bypass secret only for the test, revoke it with runlot access bypass --revoke.