Quality Engineering
DORA metrics for quality leaders
Test counts and coverage percentages tell leadership almost nothing about delivery. DORA's five metrics connect the work your test automation does to outcomes a CTO already tracks.
ReadBlog/Quality Engineering
Most load tests model a fixed number of users looping. Payday traffic arrives at a rate instead, and that difference is what breaks systems at the worst possible minute.
A load test with two thousand virtual users passes. Then salaries land, the real crowd shows up, and the service falls over at what monitoring reports as a lower load than the test applied. The test was right about the number of people. It was wrong about what a person does when the system slows down, and that single wrong assumption is why the green result did not survive contact with the real peak.
In Saudi systems this matters more than in most places, because the peaks are sharp and predictable. Salaries land on a known rhythm each month, and apps that are quiet in the early afternoon meet a wall of logins and transfers within minutes. Wallets, banking apps and e-commerce all feel it, and seasonal sales stack another spike on top. A test that does not reproduce the shape of that arrival is testing a different system than the one your customers use.
In short
The default way to write a load test is to spin up N virtual users, each in a loop: send a request, wait a moment, send the next. k6 calls this a closed model. The number of users in the system is fixed at N, and here is the catch that ruins peak tests: a looping user only starts their next request after the previous response comes back. When the system slows down, the virtual users slow down with it. They wait politely. The load you apply drops at exactly the moment the system is struggling, which is the opposite of what real traffic does.
Payday traffic follows an open model. Requests arrive at a rate set by how many people decide to open the app, and that rate is indifferent to your response time. If ten thousand people check their balance in a minute, they arrive whether each response takes two hundred milliseconds or four seconds. When responses slow, requests pile up instead of waiting their turn, and the queue is where a system tips from slow to down.
The consequence for testing is blunt. A closed-model test with a user count tuned to average load will under-test the peak, because the model cannot apply pressure faster than the system accepts it. To test payday you hold the arrival rate fixed and let response time be the thing that varies.
Autoscaling makes the distinction matter even more. A closed-model test ramps gently enough that the platform scales up in step and everything looks healthy, because the load never arrives faster than new instances can boot. Real payday traffic does not wait for your instances to boot. The arrival rate spikes in seconds, requests queue against the capacity you have at that instant, and whether you survive depends on cold-start time and connection limits in the minute or two before new capacity is ready. An open-model test is the reliable way to put pressure on that window, and that window is where systems fail first.
k6 supports this directly with the ramping-arrival-rate executor. You specify an arrival rate in iterations per time unit, k6 holds that rate regardless of response time, and it allocates virtual users as needed to keep it. The executor takes startRate, timeUnit, preAllocatedVUs, maxVUs and stages.
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
payday: {
executor: 'ramping-arrival-rate',
startRate: 50, // iterations per second; one request each here
timeUnit: '1s',
preAllocatedVUs: 200, // VUs ready before the test
maxVUs: 2000, // ceiling k6 may grow to, to hold the rate
stages: [
{ target: 50, duration: '1m' }, // warm up
{ target: 1200, duration: '2m' }, // the login burst as salaries land
{ target: 1200, duration: '5m' }, // hold the peak
{ target: 50, duration: '2m' }, // back down
],
},
},
thresholds: {
http_req_failed: ['rate<0.01'], // under 1% errors
http_req_duration: ['p(95)<800'], // 95th percentile under 800ms
dropped_iterations: ['count<1'], // k6 kept up with the target rate
},
};
export default function () {
const res = http.get('https://staging.example.test/api/balance');
check(res, { 'status 200': (r) => r.status === 200 });
}
Watch dropped_iterations as closely as the latency numbers. The metric counts iterations k6 could not start because no VU was free, usually because it hit maxVUs. When it climbs, it is a flaw in your load generator, not a result about your system. A test that reports great latency while silently dropping a third of its intended load is telling you the system was never actually pushed to the target rate. Raise maxVUs, give k6 more preAllocatedVUs to start, or run the generator from more than one machine, then run it again until dropped iterations stay at zero.
The thresholds block is what turns the run into a pass or fail. http_req_failed: ['rate<0.01'] fails the test if more than one percent of requests error, and http_req_duration: ['p(95)<800'] fails it if the 95th percentile crosses 800 milliseconds. Set these to the numbers your product owner has actually committed to, not round defaults, because a threshold is the line between a release you sign off and one you hold back.
A number pulled from the air produces a test that passes or fails for reasons unrelated to reality. The arrival rate, the endpoint mix, the data variety and the think time all come out of last month’s logs.
Use the peak minute, not the peak hour. Averaged over an hour a payday spike disappears into a gentle bump; the minute the doors open is the minute that breaks things. Take the endpoint mix from the same window, because real users do not hammer one URL. In the opening burst the traffic is mostly logins and balance checks, with transfers following once people are in, and a flat mix of everything misses the login stampede that actually falls over first.
| Profile input | Where it comes from | Why it matters |
|---|---|---|
| Peak arrival rate (req/s) | busiest minute in the access logs | sets startRate and the stage targets |
| Endpoint mix | path frequency during the peak window | login and balance dominate the opening burst |
| Account pool | anonymized user and account IDs | stops you load-testing one cached row |
| Think time | gap between a user’s requests in a session | models behavior once a user is inside |
| Peak duration | how long the spike lasts | sets the length of the hold stage |
Two data problems inflate your pass without showing up as an error. If every virtual user hits the same account, you are load-testing one row and its cache, so give the test a pool of accounts drawn from the logs. And caching flatters you in general: a test that reads the same handful of records runs warm in a way cold traffic does not, so include the variety that defeats the cache your production numbers depend on. Keep think time where it belongs, as the pause each user takes between actions. The arrival rate models how fast people show up; think time models what each one does once inside.
Note
The numbers in the script and the chart below are illustrative, not measurements of any real Saudi service. Build your own rates from your own logs. A target borrowed from someone else’s system tests their traffic shape, not yours.
The stages in the script describe a shape: a short warm-up, a two-minute ramp into the login burst, a five-minute hold at the peak, and a ramp down. Plotting the target rate across those phases makes the profile legible to anyone reviewing the test.
A single green tick tells you almost nothing. A meaningful result is the latency percentiles and error rate held at the target arrival rate: p95 and p99 under your thresholds, errors under one percent, while the rate is sustained across the whole hold stage rather than touched for a second. The hold stage is where you learn whether the system degrades gracefully or collapses, so read the percentiles across all five minutes, not the best moment in them.
Report percentiles, never averages, and know what each one is telling you. An average hides the tail, and the tail is where customers give up: a p50 of 180 milliseconds with a p99 of nine seconds means one request in a hundred is unusable, and on payday one in a hundred is thousands of people. p95 is the number most teams commit to in an SLA; p99 is the one that predicts the support tickets. Watch both as the arrival rate climbs, because a p99 that stays flat until the peak and then goes vertical is the signature of a queue filling, and the rate at which it turns vertical is your real capacity ceiling in requests per second.
The ramp into the peak deserves as much thought as the peak itself. Salaries do not arrive as a smooth two-minute slope; the real curve is closer to a step, a quiet morning and then a near-vertical rise the moment deposits post. Make the ramp stage short and steep rather than long and gentle, because a gentle ramp gives autoscaling and caches time to warm that the real event will not give them. If your monitoring shows the historical rise happening inside a minute, model it inside a minute, or the test hands the system a warm-up the day itself withholds.
Two runs beyond the basic profile earn their place. Add a headroom run above your expected peak, because the honest question is not whether you survive the forecast but how far past it you get before you break. Knowing you have, say, forty percent of headroom above the worst payday you have seen is the difference between a calm on-call rotation and a surprise. And add a soak, the same load held for an hour or more, because a certain class of failure only appears under sustained load: memory creeping up until a process is killed, a connection pool slowly exhausting, a log disk filling. A five-minute test never reaches those, and they are exactly the failures that take a service down two hours into a busy day.
Your payment gateway’s sandbox has its own rate limits, usually far below production. Hammer it in a load test and you measure the sandbox’s throttle, not your capacity, and you may get your access cut for the afternoon. Stub those downstream calls, or agree explicit test limits with the provider in advance, and be clear in the results about which dependencies were real and which were stubbed. A load test that silently mocked the slowest hop carries an asterisk that leadership needs to see, because it changes what the result means.
The dependency you do own is often the one that breaks first, and it is usually the database connection pool. Under the open model, slow queries hold connections longer, the pool drains, and new requests wait for a connection that never frees, which turns a mild slowdown into a cliff. A load test that uses a realistic account pool and realistic query variety will find that cliff. One that reads the same cached row will sail past it and report that everything is fine, right up to the morning it is not.
Run the peak test like a small operation, with the same discipline you would want on the real day.
The report that lands is short and honest. Lead with the one sentence that answers the question they are actually asking: at the target arrival rate of the busiest payday minute, the system held p95 under 800 milliseconds and errors under one percent for the full hold, with the generator never dropping an iteration. Then give the headroom factor, so they know how far past the forecast the system stands, and name the dependencies that were stubbed, so nobody mistakes a partial test for a full one. If the test failed a threshold, say which one and at what rate it broke, because “it fell over at 1,500 requests per second on the balance endpoint” is a number an engineering team can act on, while “the load test failed” sends everyone back to the logs.
Pull last month’s peak-minute request rate and endpoint mix from your access logs before anything else. That one query turns the whole exercise from a guess into a test of the day you actually have to survive.
Quality Engineering
Test counts and coverage percentages tell leadership almost nothing about delivery. DORA's five metrics connect the work your test automation does to outcomes a CTO already tracks.
ReadCompliance
Saudi controllers have 72 hours to notify SDAIA after becoming aware of a personal data breach. The hour-by-hour decisions you make, and the work you did before, decide whether you can.
ReadCybersecurity
A chatbot or RAG copilot inside a banking app is a new attack surface running on old failure modes. The OWASP LLM Top 10 names them; here is how we design and test around the ones with no clean fix.
ReadEhsanLab
Tell us what you are building and what worries you about it. A specialist replies within one business day.