Module 4: Load Profiles And Stages
4. The shapes: constant, ramp, and spike
Overview
With stages you can draw almost any load shape, but in practice three shapes cover most questions, and it's worth knowing them by name because each reveals something different. The constant (a flat plateau) reveals the stable state. The ramp (a gradual rise) reveals where degradation begins —the capacity knee—. And the spike (an abrupt rise) reveals resilience to a sudden peak and whether the system recovers from the blow. The difference between the ramp and the spike is subtle but enormous: both take the load to a high peak, but one does it slowly and the other all at once, and the system reacts very differently to each. In this lesson we walk through the three shapes, when to use each, and above all we run a real spike against Reservo so you see with numbers why "rising all at once" isn't the same as "rising slowly to the same point."
Connection to the module: lesson 3 gave you the syntax (stages); this one gives you the vocabulary of shapes you write with that syntax. It's the bridge to lesson 7 (choosing the profile according to the question): there you'll decide which shape to use; here you learn what each one is and what it reveals. We reuse the staged ramp you already ran (the staged_load.py generator's) as an example of the "ramp" shape, and add a new generator, spike_load.py, for the "spike" shape. The constant shape we already discussed in lesson 2 (the isolated peak stage). The executors that implement these shapes underneath are lessons 5 and 6; here we stay on the shapes themselves.
The analogy: three ways to get people into a theater
Imagine you manage a thousand-seat theater and want to know how the entrance behaves. There are three different experiments, and they're the three load shapes.
Constant: you let people in at a fixed rate, say 100 people per minute, for half an hour, and observe. You learn how the entrance flows in stable regime at that rate: whether the lines stay short, whether the ushers keep up. It's the plateau.
Ramp: you start letting people in slowly and keep increasing the rate little by little —50, 100, 200, 400 per minute— until saturation. Since you rise gradually, you see exactly at what rate the bottleneck starts to form: maybe at 300 per minute the lines still flow, but at 350 they jam. That point —the knee— is what the ramp reveals and the constant doesn't.
Spike: the theater is calm with few people and suddenly, when a game at the stadium next door ends, a thousand people appear all at once at the door in two minutes. It's not a gradual increase: it's a wall of people. Here you don't care about the sustained rate; you care whether the system survives the blow —do the doors collapse? do people get trampled?— and, crucially, whether it recovers when the avalanche passes —does the entrance flow normally again in five minutes, or is there chaos that takes half an hour to clear?—. A theater can perfectly handle 1000 people spread over an hour (ramp) and still collapse with those same 1000 in two minutes (spike). Same total, different shape, opposite result.
Shape 1: constant (the plateau)
The constant shape is a fixed load throughout the whole test: a single plateau, with no appreciable rise or fall. In stages it's written with one segment (or two, if you want a fast rise before the plateau) that holds the target:
// CONTENT (not run here). Constant shape: 20 fixed VUs for 1 minute.
export const options = {
stages: [
{ duration: '1m', target: 20 }, // rises to 20 and, being the only target, is almost all plateau
],
};
What it reveals: the behavior in steady state at that load. It's the shape of the smoke test (few VUs, confirm it works), of verifying an SLO at the expected load, and of the soak (a long constant to look for leaks). You already saw it executed in lesson 2: the isolated peak stage, 24 VUs → 174.63 ms p95. Its limit, as we argued, is that it says nothing about the rise, the limit, or the recovery. It's the right shape when the question is about the stable regime.
Shape 2: ramp (the gradual rise)
The ramp takes the load from little to a lot gradually, traversing the intermediate levels. It's the ramp-up you already know, but used as the protagonist: you rise slowly and observe at which level the system starts to suffer.
// CONTENT (not run here). Ramp: gradual rise from 0 to 100 VUs in 2 min.
export const options = {
stages: [
{ duration: '2m', target: 100 }, // rises linearly from 0 to 100 VUs
],
};
What it reveals: the degradation knee —the point where the p95 stops growing smoothly and spikes— and, if you keep rising until something yields, the breaking point (the stress test). Since the load rises gradually, the ramp traverses 10, 20, 40, 80 VUs and shows you the complete latency-vs-load curve, not just a point.
You already ran it: the staged_load.py generator is a staged ramp. Its p95 column —22.55 → 77.01 → 174.63 on the rise— is exactly the curve the ramp reveals. Look at it as "the latency as a function of the load": at 4 VUs, 22 ms; at 12, 77 ms; at 24, 175 ms. That ascending and accelerating curve is the signature of a system approaching saturation, and only a ramp draws it.
Shape 3: spike (the abrupt rise)
The spike takes the load to a high peak all at once, holds it briefly, and lowers it. The difference from the ramp isn't the destination (both reach a peak) but the speed: the spike gives no time for warm-up or adaptation. It models a sudden peak of real traffic —a promotion going viral, a link on a TV show, the end of a game—.
// CONTENT (not run here). Spike: low baseline, abrupt jump, and return.
export const options = {
stages: [
{ duration: '30s', target: 3 }, // baseline: normal load, low
{ duration: '10s', target: 30 }, // SPIKE: abrupt jump to 30 VUs in 10 s
{ duration: '30s', target: 3 }, // recovery: return to baseline, observe recovery
],
};
What it reveals: two things the ramp doesn't. First, the resilience to the blow: when 30 users appear all at once, the system had no time to warm caches or open connections, so the p95 jumps much more than it would rising gradually to the same peak. Second —and it's the spike's jewel—, the recovery: after the peak, does the system quickly return to its baseline latency, or does it stay damaged? A system whose queues fill all at once can take a while to drain them long after the peak passed.
The executed spike
Here's the real spike against Reservo, with the three phases: baseline (3 VUs), spike (jump to 30 VUs), recovery (return to 3 VUs).
What to expect — the p95 should be low and stable at the baseline, jump hard at the spike (with a very high maximum, because the blow catches the system cold), and return to the baseline on the recovery if the system is healthy. Real output:
$ python3.14 spike_load.py http://127.0.0.1:PORT
SPIKE profile against http://127.0.0.1:PORT/quote
phase VUs requests p95 (ms) max (ms)
--------------------------------------------------
baseline 3 993 15.82 30.80
SPIKE 30 1171 235.01 2016.39
recovery 3 995 15.41 28.33
--------------------------------------------------
errors: 0
Read the three phases. At the baseline (3 VUs), the p95 is 15.82 ms and the maximum 30.80 ms: all calm, the system at rest. The spike arrives (30 VUs all at once) and the p95 spikes to 235.01 ms —fifteen times the baseline— but look above all at the max column: 2016.39 ms, over two seconds! That maximum is the signature of the blow: when 30 users appear at once, the first requests form an enormous queue while the system absorbs the avalanche, and some wait two seconds. The gradual ramp to the same peak of 24-30 VUs never produced a maximum this brutal, because it rose giving time to assimilate. And on the recovery (return to 3 VUs), the p95 returns to 15.41 ms and the maximum to 28.33 ms: identical to the baseline. Reservo recovered from the blow cleanly and instantly. That return is the good news the spike was designed to verify.
The ramp and the spike reach the same peak, but the ramp rises slowly and the spike all at once. The blow reveals two things the gradual rise hides: how much worse the peak is with no warm-up time (look at the
max, not just the p95) and whether the system recovers when the avalanche passes. Same total load, different shape, different behavior.
Ramp vs spike: why the speed changes everything
It's worth insisting, because it's the heart of the lesson. Compare the ramp's peak (24 VUs, rising gradually: p95 174.63 ms, and its moderate maximums) with the spike's peak (30 VUs, all at once: p95 235.01 ms, maximum 2016.39 ms). Although the spike has only a few more VUs, its behavior is qualitatively worse because of the shape, not the size. When the load rises gradually, each new VU arrives at a system that already adapted to the previous ones; when it rises all at once, they all arrive at a system that was still at rest, and the cold start (empty caches, unopened connections, queues that fill in one shot) punishes the first requests with enormous waits.
That's why you can't replace a spike test with a "faster" ramp: the abruptness is the variable you're testing. And that's why the max (or the p99) matters so much in a spike: the average and even the p95 can look acceptable, but the handful of requests that caught the blow at the worst moment saw latencies of seconds, and those are real users who lived the avalanche. A system that rises all at once and doesn't recover —that stays slow minutes later— has a serious resilience problem that only a spike test uncovers.
Common mistakes
Confusing a fast ramp with a spike. What happens: someone sets a short ramp-up (rising to 30 in 5 seconds) and thinks they tested a spike. Why it happens: both reach the peak fast. How to detect it: a real spike jumps from a low baseline to a high peak almost instantly and then returns, to measure the blow and the recovery; a ramp, however fast, still traverses levels. How to fix it: if your question is "does it survive a sudden peak and recover?", use the spike shape (baseline → jump → baseline) and look at the max and the recovery phase, not just the peak's p95.
Looking only at the p95 in a spike and ignoring the max. What happens: in the spike the p95 was 235 ms and someone concludes "acceptable." Why it happens: the p95 is the metric we always look at. How to detect it: in a spike, the blow's damage lives in the extreme tail —the max of 2016 ms—, which the p95 (which leaves out the worst 5%) doesn't capture. How to fix it: in a spike, look at the max and the p99 too; they're the requests that lived the avalanche, and they're real users.
Not including the recovery phase. What happens: the spike test rises all at once, measures the peak, and ends there. Why it happens: it's believed that "it held the peak" is the conclusion. How to detect it: without a return-to-baseline phase, you don't know whether the system drained the queues or stayed stuck; half the spike's value (does it recover?) is lost. How to fix it: always close the spike with a return to the baseline and compare the post-peak latency with the pre-peak; if it doesn't return, there's a leak or a queue that doesn't drain.
Exercises
Exercise 1 — Name the shape. For each stages, say whether it's constant, ramp, or spike. (a) [{duration:'2m',target:50}] (a single long rise to 50). (b) [{duration:'20s',target:10},{duration:'10s',target:200},{duration:'20s',target:10}]. (c) [{duration:'10s',target:30},{duration:'5m',target:30},{duration:'10s',target:0}].
See solution
- (a) Ramp. A gradual, sustained rise from 0 to 50 in 2 minutes; it traverses the intermediate levels. (No explicit plateau: it's a pure ramp.)
- (b) Spike. Low baseline (10 VUs), abrupt jump to 200 in 10 seconds, and return to 10. The abruptness of the jump and the return to baseline give it away.
- (c) Constant (with a fast rise). It rises to 30 in 10 seconds and holds 30 for 5 minutes —a long plateau—; the protagonist is the plateau, not the rise. The long duration even suggests a soak.
Exercise 2 — Read the spike. With the spike's executed output, answer: (a) How many times higher is the spike's p95 relative to the baseline? (b) Why is the spike's max (2016 ms) so disproportionate to its p95 (235 ms)? (c) What does it tell you that the recovery phase has p95 15.41 ms, almost equal to the baseline 15.82 ms?
See solution
- (a)
235.01 / 15.82 ≈ 15times. The blow multiplied the p95 by fifteen. - (b) Because the blow (30 VUs all at once on a system at rest) punishes a handful of requests very hard —the first of the avalanche, which form an enormous queue—, which go to the extreme tail (the
max). The p95 leaves out that worst 5%, so it doesn't see the two seconds; themaxdoes. In a spike, the extreme tail is where the damage lives. - (c) That Reservo recovered cleanly and instantly: when the avalanche passed, it drained any queue and returned to its rest latency. If the recovery had stayed at, say,
100 ms, you'd suspect something didn't get released after the blow.
Exercise 3 — Choose and write the shape. For each question, say which shape you'd use and write an example stages. (a) "At what load level does Reservo start to degrade?" (b) "Does Reservo survive 500 users arriving all at once when the campaign goes live, and does it recover?"
See solution
-
(a) Ramp. A gradual rise that traverses the levels until you see the knee:
stages: [ { duration: '3m', target: 200 } ] // rises from 0 to 200, observe where the p95 bendsYou look at the p95-vs-load curve and find where it stops being smooth.
-
(b) Spike. Low baseline, abrupt jump to 500, and return to baseline to see the recovery:
stages: [ { duration: '30s', target: 20 }, // baseline { duration: '10s', target: 500 }, // SPIKE: sudden blow { duration: '1m', target: 500 }, // hold the peak a bit { duration: '30s', target: 20 }, // recovery: return to baseline ]You look at the
max/p99 during the spike (the blow) and compare the recovery's p95 with the baseline's (the recovery).
Summary and next step
In this lesson you learned the three canonical load shapes and what each reveals. The constant (plateau) reveals the stable state: the shape of the smoke, the SLO at expected load, and the soak. The ramp (gradual rise) reveals the degradation knee and the breaking point: it traverses the intermediate levels and draws the latency-vs-load curve. And the spike (abrupt rise) reveals the resilience to the blow and the recovery: it takes the load to a peak all at once, with no warm-up time.
The big lesson is that the ramp and the spike reach the same peak but by opposite paths, and the system reacts differently to each. You saw it with real numbers: the executed spike jumped to a p95 of 235 ms with a brutal max of 2016 ms —the cold-start signature— and then recovered cleanly to 15 ms, identical to the baseline. The ramp to the same peak never produced that maximum, because it rose giving time. In a spike, the max and the p99 matter as much as the p95, because the blow lives in the extreme tail.
Before moving on you should be able to: name the three shapes and what each reveals; explain why a spike isn't a fast ramp; read a spike's output distinguishing the blow (the max) from the recovery (the p95 back to the baseline); and write a stages for each shape.
What comes next is looking under the hood. So far you drew shapes with stages without asking what engine executes them. In lesson 5 we meet k6's executors —the engines that decide how the load is applied— starting with the two that work with VUs: constant-vus (fixed VUs) and ramping-vus (the one underneath stages).
Resources
- k6 — Test types: load, stress, spike, soak — the official guide that describes each shape and what it reveals; the direct reference of this lesson.
- k6 — Spike testing — the detail of the spike shape: why the abruptness matters and what to observe (the blow and the recovery).
- k6 — Stress testing — the ramp shape taken to the limit to find the breaking point; it connects with lesson 7.
concurrent.futures— Python documentation — the tool the spike generator launches the avalanche of concurrent workers with that models the blow.