Module 4: Load Profiles And Stages
2. Why a constant load isn't enough
Overview
A constant load is the easiest test to write: you fix N users, hold them for N minutes, measure. And it's not bad —it answers a legitimate question—. The problem is that it answers only one, and real traffic poses several. In this lesson we take apart the constant load to see precisely what it does answer and what it leaves mute, and from there the module's central idea is born: that the load needs a shape. The thesis is short: real traffic isn't flat, it arrives in waves, and a flat test can't describe a wave. With a constant load you know how you do on a plateau; you don't know how you reach that plateau, at what point of the rise you started to suffer, or how you recover when the wave drops. Those three things —startup, degradation point, and recovery— are exactly the ones a load shape reveals and a constant one hides.
Connection to the module: lesson 1 gave you the big idea (size vs shape) and an executed preview. This lesson argues it: why the shape is necessary, not a luxury. It's the "why" that justifies everything that comes after —the stages (lesson 3), the concrete shapes (lesson 4), the executors (5 and 6)—. Here we still use the by-VU concurrency vocabulary from module 2 and the metrics from module 3 (the p95); what's new is starting to look at them over time instead of at a single instant. We don't touch the anatomy of stages yet (that's lesson 3): we stay on the argument and on an executed contrast between a flat load and a shaped one.
The analogy: the photo and the film
Think of the difference between a photo and a film. A constant load is a photo: it captures an instant —"this is how the system looks with 24 users on it"— and that instant is real and useful. But a photo has no before or after. It doesn't show how the system reached that state or what happened afterward. If I show you the photo of a runner mid-race, you know that at that moment they were running, but not whether they were accelerating or braking, nor whether a second later they collapsed.
A load profile is a film: a sequence of instants that tells a story with a beginning, middle, and end. It shows the rise (how the system absorbs a growing demand), the peak (how it holds at the highest point), and the fall (how it recovers). And in that sequence things appear that no single photo can show: the exact moment the p95 starts to spike while you rise, or the revealing fact that, after the peak, the system does not return to its rest latency —a clue that something got stuck—. The photo (constant load) isn't false; it's incomplete. And when the question is "how does my system behave under a real day's traffic?", which is a story with waves, you need the film.
What a constant load DOES answer
Let's be fair to the constant load before criticizing it, because it has its place. A flat plateau of N users answers one question well: "how does the system behave in a stable state with N users on it?". It's the question of the steady state, and it's valuable. If you know your peak traffic is 24 sustained concurrent users, a constant load of 24 tells you the p95 they'll see in that peak hour. For a smoke test —does the system even work under a minimal load?— the constant is the ideal shape: a few fixed VUs, a few seconds, and you confirm nothing explodes before investing in bigger tests.
Look at the real number. Running the generator at a constant concurrency of 24 (the peak phase, isolated) against Reservo:
What to expect — with 24 fixed users, the p95 stabilizes at a value; it's the steady-state latency at that load. Real output (the peak stage of a staged run, which is exactly a constant load of 24):
stage VUs requests p95 (ms) average (ms)
steady (peak) 24 1775 174.63 63.00
That 174.63 ms is good and honest information: "in a stable state, with 24 concurrent users, 95% of the quotes respond in 175 ms or less". If your SLO were "p95 < 500 ms", this photo would already tell you that at 24 users you meet it with room to spare. The constant load did its job. The problem isn't that it lies; it's everything that's not in the photo.
What it does NOT answer (and why it hurts)
A constant load of 24 users gives you that 174.63 ms and nothing more. These are the three questions it leaves mute, and why each one matters:
How you behave while you rise. The steady-state number says nothing about the path to it. Does the p95 rise smoothly and linearly with the load, or is there a knee —a point where it suddenly spikes—? That knee is gold information: it marks where the real degradation begins, your useful capacity. A load that starts straight at 24 never visits the intermediate levels, so it can't find the knee. Only a rise (a ramp-up) traverses 4, 8, 12, 16, 20, 24 and shows you at which one the curve bent.
Where your limit is. A constant at 24 confirms 24 goes well, but it doesn't look for the breaking point. Do you handle 50? 100? Beyond how many does the p95 become unacceptable or the errors begin? That question —the stress test of lesson 4— is only answered by raising the load beyond normal until something yields. A fixed plateau, by definition, never tries.
Whether you recover. This is the most treacherous one. After a load peak, does the system return to its rest latency, or does it stay degraded? A healthy system recovers; one with a leak (memory not released, connections not closed, a queue that doesn't drain) stays slow even when the load has already dropped. A constant load has no "after the peak" —it ends at the peak—, so it's structurally incapable of seeing the recovery. You need a fall (a ramp-down) to observe it.
The executed contrast: a photo vs the film
Let's put the two tests side by side, both against the same Reservo API, with the same users at the peak. The difference is only the shape: one is flat (constant at 24), the other has shape (rises and falls).
What to expect — the constant load delivers a single number (the peak's); the shaped load delivers the complete story, with the p95 rising on the ramp and recovering on the fall. Real output of the staged run:
$ python3.14 staged_load.py http://127.0.0.1:PORT
staged profile against http://127.0.0.1:PORT/quote (Focus/basic/3h -> 7500)
stage VUs requests p95 (ms) average (ms)
--------------------------------------------------------------
ramp-up (warm) 4 1028 22.55 12.17
ramp-up (mid) 12 1208 77.01 33.18
steady (peak) 24 1775 174.63 63.00
ramp-down (mid) 12 1187 82.56 33.65
ramp-down (cool) 4 1054 24.20 11.85
--------------------------------------------------------------
errors: 0
A constant load of 24 would have given you a single one of those rows: the peak's, 174.63 ms. The shaped load gives you all five, and with them three things the constant never:
- The startup: with 4 users the p95 is
22.55 ms. Now you know your almost-at-rest latency, your baseline. - The rise: from 4 to 12 to 24, the p95 goes
22.55 → 77.01 → 174.63. You see how it grows with the load —here, more than linearly: tripling from 4 to 12 multiplied the p95 by more than three, and doubling from 12 to 24 by more than two—. That acceleration is the signal you're approaching saturation. - The recovery: on the way back down to 12 and to 4, the p95 returns to
82.56and24.20—almost identical to the77.01and22.55on the way up—. The system recovers cleanly: no damage remained. If the "ramp-down (cool)" row had shown, say,150 mswith only 4 users, you'd have an alarm —something didn't get released after the peak— and a constant load would never have lit it.
The same API, the same module-3 metric, the same peak. The only thing that changed was giving the load a shape, and with that you went from a photo to a film.
A constant load answers "how do you do in a stable state with N users?" —a photo of an instant—. Real traffic is a film: it rises, holds, falls. Only a shaped load reveals the startup (where does it start to hurt?), the limit (where does it break?), and the recovery (does it return to normal?). The constant doesn't lie; it's incomplete.
When the constant is the correct shape
None of this means the constant load is bad; it means it's one of several shapes, each for a question. The constant is the right choice when the question is about the steady state:
- Smoke test: a few constant VUs for a short time, only to confirm the system starts and responds. The simplest shape for the most basic question.
- Verifying an SLO at the expected load: if you know your sustained peak is 24 users and you want to confirm that there you meet "p95 < 500 ms", a constant at 24 is exactly what you need.
- Soak test: a long constant (hours) to look for slow degradation —leaks that only appear over time—. Here the flat shape is the right one, but the duration is the key variable (we'll see it in lesson 7).
The rule isn't "never use constant load"; it's "choose the shape according to the question". If your question has to do with the rise, the limit, or the recovery, the constant isn't enough and you need a shape with stages. That conscious choice is the subject of lesson 7.
Common mistakes
Reporting the steady-state number as if it described "the system." What happens: someone runs a constant at 24, gets 175 ms p95, and presents it as the performance of the system. Why it happens: a single number is comfortable to communicate. How to detect it: if your conclusion is a number with no shape behind it, you described a photo and sold it as a film. How to fix it: accompany the steady-state number with the shape —how you got there and whether you recover—; the peak's p95 is one row of the table, not the whole table.
Starting the load straight at the peak. What happens: the test starts with the 24 VUs all at once, with no rise. Why it happens: it seems that "going straight to the point" saves time. How to detect it: if your first instant of load is already the maximum, you skipped the whole rise and can't see the degradation knee (besides punishing the system with a cold start that doesn't represent real traffic, which rises gradually). How to fix it: use a ramp-up —a gradual rise— so the test traverses the intermediate levels and gives warm-up time; that's what stages does (lesson 3).
Ending the test at the peak and not observing the fall. What happens: the test cuts off right when the load is at maximum, with no ramp-down. Why it happens: it's assumed that "if it held the peak, it's fine." How to detect it: if your test has no "after the peak," you can't see whether the system recovers —and a leak escapes you completely—. How to fix it: add a fall phase and compare the fall's latency with the rise's at the same load level; if it doesn't return, there's something to investigate.
Exercises
Exercise 1 — What question does it answer? For each goal, say whether a constant load is enough or whether you need a shaped load (and why). (a) "Confirm the API starts and responds 200 under a minimal load." (b) "Find beyond how many users the p95 spikes." (c) "Know the p95 at our sustained peak hour of 24 users." (d) "Verify that after a traffic peak the latency returns to normal."
See solution
- (a) Constant is enough. It's a smoke test: a few fixed VUs to confirm it works. The question is about a state, not an evolution.
- (b) Shaped (increasing ramp). "Beyond how many it spikes" requires raising the load and observing the knee; a fixed plateau never visits the intermediate levels or looks for the limit.
- (c) Constant is enough. The question is about the steady state at a known load; a constant at 24 answers it directly.
- (d) Shaped (needs a ramp-down). The recovery is only seen if there's an "after the peak"; a constant that ends at the peak is incapable of showing it.
The rule: if the question is about a state, constant; if it's about a rise, a limit, or a recovery, shaped.
Exercise 2 — Read the film. With the executed output of the staged run, answer: (a) What's the system's baseline latency (almost at rest)? (b) Between 4→12 VUs and 12→24 VUs, in which segment does the p95 grow proportionally more, and what does that suggest? (c) Which row would you use to report "the p95 at peak hour" and which to argue "the system recovers"?
See solution
- (a) The 4-VU one:
22.55 ms(or the24.20of the fall to 4). It's the baseline, the almost-contention-free latency. - (b) From 4 to 12 (×3 the load) the p95 went from
22.55to77.01(×3.4). From 12 to 24 (×2 the load) it went from77.01to174.63(×2.3). In both segments the p95 grows more than proportionally to the load, which suggests there's already contention and you're approaching saturation; it's not a comfortable linear growth. - (c) For "p95 at peak hour," the
steady (peak)row with174.63 ms. For "it recovers," you compareramp-down (cool)(24.20) withramp-up (warm)(22.55): almost equal, hence a clean recovery.
Exercise 3 — Redesign the test. A colleague tests Reservo like this: "I launch 24 constant VUs for 30 seconds and report the p95." Their boss asks three things: "where does it start to degrade? does it handle 50? does it recover after the peak?". Rewrite the test (in words, describing the shape) so it answers all three, and say which phase answers each question.
See solution
A shaped test, for example: rise from 0 to 50 VUs in stages (ramp-up), hold a peak, and lower back to a few (ramp-down). With that:
- "Where does it start to degrade?" → the rise: traversing 4, 12, 24, 36, 50 you see at which level the p95 bends (the knee).
- "Does it handle 50?" → taking the ramp-up (or the steady) up to 50 answers whether at that load the p95 is still acceptable or already spiked; if you want the exact limit, keep raising until it yields (stress).
- "Does it recover?" → the fall: you compare the p95 on the fall with that of the rise at the same level; if it returns, it recovers.
The constant at 24 answered none of the three; it only gave the steady-state number at 24. Each of the boss's questions is about a phase of the shape.
Summary and next step
In this lesson you argued why a constant load isn't enough. A constant answers one question well —"how do you do in a stable state with N users?"—, and that photo is real and useful (for a smoke test, to verify an SLO at the expected load, for a long soak). But real traffic isn't a photo, it's a film: it arrives in waves that rise, hold, and fall. And there are three things only the film reveals —the startup (where does the rise start to hurt?), the limit (where does it break?), and the recovery (does it return to normal after the peak?)— that a flat plateau hides by construction.
You saw it with numbers: the peak stage gave 174.63 ms, but the shaped run gave all five rows —the 22.55 ms baseline, the accelerated rise, and the symmetric recovery back to 24.20 ms—. The same API, the same metric; what changed was giving the load a shape. And you saw that the constant isn't "bad": it's one of several shapes, correct when the question is about a steady state, and insufficient when the question is about an evolution.
Before moving on you should be able to: name the three questions a constant load leaves mute; explain why the constant is structurally incapable of seeing the recovery; say when the constant is the right shape; and read a p95-per-stage table distinguishing the baseline, the rise, and the recovery.
What comes next is learning to write that shape. In lesson 3 we open the anatomy of k6's stages: what exactly that list of {duration, target} is, how k6 interpolates the VUs toward each target, and how the three phases —ramp-up, steady, ramp-down— are read in a real script (content) and in the Python generator (executed).
Resources
- k6 — Load test types — the official guide that distinguishes smoke, load, stress, and spike by their shape; it connects this argument with the types you'll see in lesson 7.
- k6 —
stagesoption — how the shape (ramp-up/steady/ramp-down) that the constant load lacks is written; we open it in lesson 3. - Google SRE Workbook — Implementing SLOs — why performance is observed as a series over time, not as an isolated number; the foundation of "the film over the photo."
statistics— Python documentation — the library the generator computes each stage's p95 with (the same one from module 3, now applied per segment).