← Open in the full interactive course (progress tracking, search & more)

Passing your performance target at 500 requests per second tells you nothing about what happens at 5,000 — or exactly where, between those two numbers, the system stops coping gracefully and starts failing outright.

Lesson 313 gave your system a target — p95 under 200ms at 500 req/s — and a way to prove it's met. That's genuinely valuable, and it's also a deliberately narrow question: it only tells you about behavior at that one, expected load level. It says nothing about what happens on the day traffic is triple the forecast, or a marketing campaign lands harder than planned, or one dependency slows down and requests start piling up. Those are the moments that actually take a system down — and performance testing, by design, never looks there.

Load testing — and its more aggressive sibling, stress testing — exist specifically to look there. Instead of asking "does it meet the bar at the load I expect," they ask a harder, more useful question: "as load climbs toward and past what I expect, where and how does this system actually break?"

In this lesson, you'll learn how load and stress testing differ from performance testing (313), the three shapes a load test typically takes — steady, spike, and soak — and what "breaking" actually tends to look like in a real .NET web application: thread pool starvation, connection pool exhaustion, GC pressure, and database bottlenecks. You'll also see exactly where this Part's story turns from testing toward production diagnosis.

What Is It?

The Simple Explanation

Load testing pushes concurrent traffic up toward — and, in its more aggressive form (stress testing), well past — the level a system is actually expected to handle, specifically to find out where it stops coping gracefully and how it fails once it does.

The Technical Definition

Where performance testing (313) validates a system against a fixed target at expected load, load and stress testing deliberately vary load upward — often well beyond what's expected — to characterize the system's breaking point: the load level at which throughput stops scaling, error rates climb, and latency degrades sharply rather than gradually. It's a search for a boundary, not a pass/fail check against one predetermined number.

Performance testing (313)

Load / stress testing (this lesson)

Why Does It Exist?

The Problem — "Meets the Target" Doesn't Mean "Fails Gracefully Beyond It"

Real traffic doesn't politely stay at the level you forecast. A system that meets its target perfectly at 500 req/s can behave in wildly unpredictable ways at 1,500 — sometimes it degrades gracefully (a little slower, still correct), and sometimes it falls off a cliff (timeouts cascade, connections exhaust, the whole thing becomes unresponsive well before you'd expect from a linear extrapolation). Nobody actually knows which of these will happen — or at what exact load level — without deliberately testing for it.

The Solution — Find the Breaking Point on Your Own Terms, Before Reality Finds It for You

Load and stress testing exist to discover that boundary deliberately, in a controlled setting, rather than discovering it for the first time during a real traffic spike in production. Knowing your system starts genuinely struggling around 1,800 req/s — and specifically why — is exactly the kind of information capacity planning, alerting thresholds, and incident preparedness all depend on.

Big Picture — Three Shapes of Load Test

STEADY, SPIKE, AND SOAK — DIFFERENT QUESTIONS, DIFFERENT SHAPES
STEADY / RAMPING LOAD TEST
SPIKE TEST
SOAK / ENDURANCE TEST

How It Works — Watching the Curve Bend

A healthy system's throughput rises roughly in step with load, for a while. The moment it stops doing that — throughput plateaus or drops while load keeps climbing, and latency starts rising sharply instead of gradually — you've found the "knee" in the curve: the point where the system transitions from coping to struggling. Load testing is fundamentally about locating that knee deliberately, on purpose, rather than discovering it live.

Region of the curveWhat's happening
Below the kneeThroughput scales roughly linearly with load; latency stays flat and low
Near the kneeLatency starts climbing noticeably even as throughput growth slows — the earliest real warning sign
Past the kneeThroughput stalls or falls even as load keeps increasing; error rates climb; the system is now actively struggling, not just slower

Simple Example — a Ramping k6 Script to Find the Ceiling

Rather than a fixed vus count as in lesson 313's target-check script, a load test ramps upward in explicit stages:

import http from 'k6/http'; export const options = { stages: [ { duration: '2m', target: 200 }, // ramp up to 200 VUs { duration: '2m', target: 800 }, // ramp up further { duration: '2m', target: 2000 }, // push well past expected peak { duration: '2m', target: 0 }, // ramp back down ], }; export default function () { http.get('https://staging.example.com/api/orders/1'); }

Meaning: Rather than asserting a fixed threshold and stopping there, this script's whole purpose is to watch how latency, throughput, and error rate change as VUs climb from 200 to 2,000 — the interesting output isn't a single pass/fail, it's the shape of the curve, and the specific VU count where that curve visibly bends.

Real-World Example — What "Breaking" Actually Looks Like in a .NET Web App

The knee in the curve isn't a mysterious, unknowable event — in a real ASP.NET Core application, it almost always traces back to one of a small, well-known set of resource limits being hit:

Thread pool starvation
Not enough worker threads to keep up with incoming work — covered in depth in lesson 319
Connection pool exhaustion
Every available database (or HTTP) connection is already checked out; new requests queue or fail outright
GC pressure
Allocation rate under load triggers frequent, increasingly long garbage collection pauses that steal CPU from real work
Database bottleneck
The database itself becomes the ceiling — explored fully in lesson 320

A load test's real value is exactly this: it doesn't just tell you a number, it hands you a concrete, reproducible failure to go investigate — turning "the system fell over at some point during a spike" into "throughput plateaued at roughly 1,800 req/s, latency spiked, and error responses started around the same point," a starting point any of the production-debugging lessons ahead (315 onward) can actually work from.

Analogy

A Bridge's Rated Load, and Its Actual Breaking Load

A bridge engineer doesn't just confirm the bridge holds its expected daily traffic comfortably — that's the equivalent of lesson 313's performance test, and it's necessary but incomplete. Separately, they load-test the structure well past its rated capacity, deliberately, in a controlled setting, to know exactly how much weight it can bear before it genuinely fails, and which specific part fails first — a support beam, a cable, a joint. That knowledge doesn't mean the bridge is meant to carry that much weight daily; it means the engineers know precisely where the real margin is, and exactly what gives way first if that margin is ever exceeded for real. Load testing a web application is the same discipline, aimed at thread pools, connection pools, and database capacity instead of steel.

Under the Hood — Why a Spike Fails Differently Than a Ramp

A gradual ramp gives every layer of the system time to adapt — connection pools grow toward their configured maximum, auto-scaling (if configured) has time to add capacity, caches warm up under the increasing traffic. A spike test denies the system all of that adjustment time: load jumps from normal to extreme in seconds, and every layer has to absorb the shock with whatever capacity it already had provisioned at that exact instant. This is precisely why a system that survives a slow ramp to 2,000 req/s can still fall over at a sudden spike to the same 2,000 req/s — the failure mode isn't purely about the peak number, it's about how much time the system had to prepare for it. Testing only one shape (usually the gentler ramp) and assuming it covers the other is a common, costly blind spot.

Common Confusion

1. "Load testing and performance testing are the same thing" — they ask different questions

Performance testing (313) checks a fixed target at expected load and produces a pass/fail. Load testing deliberately climbs past expected load looking for where and how the system breaks, and produces a characterized boundary rather than a simple pass/fail. A team can legitimately pass every performance test and still have never discovered where their actual breaking point is — the two disciplines are complementary, not redundant.

2. "A short, high-load test covers everything a long soak test would" — it doesn't

Some failure modes — a memory leak (316), a slowly-growing unbounded cache, a resource that's leaked a tiny amount per request — are invisible over a two-minute test and glaringly obvious over a two-hour one. Duration is its own dimension, independent of peak load, and a soak test is the only shape of test that specifically targets it.

Common Mistakes

Mistake 1 — Only ever testing a gradual ramp, never a sudden spike

Assuming a system that handles a slow climb to peak load will handle the same peak arriving suddenly — as the "Under the Hood" section explained, these are genuinely different failure modes.

Test both shapes deliberately when a sudden-traffic scenario is realistic for your system — a product launch, a marketing push, a viral moment — since a spike test can surface problems a ramp never will.

Mistake 2 — Running soak tests too briefly to actually catch what they're for

A "soak test" that only runs for ten minutes — long enough to feel thorough, far too short to reveal a slow leak that only becomes measurable after hours of sustained load.

Run soak tests for a duration that genuinely resembles the timescale you care about in production — hours, not minutes — specifically because that's the timescale a leak (316) actually needs to become visible.

Mistake 3 — Assuming the bottleneck will always be raw CPU

Watching only CPU usage during a load test and concluding "it's fine" because CPU never maxed out — while the real ceiling was thread pool starvation, connection pool exhaustion, or a database limit the whole time.

Watch the full set of real culprits this lesson named — thread pool queue length, connection pool usage, GC frequency, and database latency — not CPU alone. Raw CPU exhaustion is only one of several common ways a .NET web app actually hits its ceiling.

When Should I Use It?

Rule of thumb: Performance test (313) to prove you meet your target. Load/stress test to find out what happens beyond it, and specifically why. Both belong in a mature testing strategy — neither substitutes for the other.

Mental Model

Performance testing = pass/fail at expected load.
Load/stress testing = climb past expected load to find, and characterize, the breaking point.
Steady, spike, soak = three different shapes, revealing three different kinds of failure.
Common breaking points = thread pool starvation, connection pool exhaustion, GC pressure, database bottlenecks.

Finding the breaking point on your own terms, deliberately, is always better than a real traffic spike finding it for you.

Key Takeaway — and Where This Part Turns Next

And that's exactly the boundary this lesson sits on. Every test in this Part so far has run in a controlled environment, against a system you could stop, reset, and re-run at will. Production doesn't offer that courtesy — when a system fails under real, live load rather than a test harness, you can't simply pause the world and try again. The moment a breaking point like the ones this lesson described shows up for real — in production, under real traffic, with real consequences — the discipline shifts from testing to diagnosis. That's exactly where the rest of this Part goes next: lesson 315 onward covers how to actually find and fix a memory leak, runaway CPU, a deadlock, thread pool starvation, and a database bottleneck when they're happening live, not in a lab.


Check Your Understanding

You've learned how load testing differs from performance testing, the three shapes a load test can take, and what "breaking" typically means in a real .NET application. Let's confirm it clicked.

1. A system passes its performance test (313) at 500 req/s with p95 under 200ms. What does this tell you about its behavior at 3,000 req/s?

Show answer

Correct: B

Why B is correct: This is the lesson's central distinction — a performance test's pass/fail result is specific to the load level it was run at, and says nothing reliable about behavior at a meaningfully higher load. That's precisely the gap load/stress testing exists to fill.

Why A is incorrect: Behavior at 500 req/s provides no guarantee about behavior at 3,000 req/s — systems can degrade non-linearly, sometimes sharply, well before a naive extrapolation would suggest.

Why C is incorrect: This overstates the opposite direction — many systems handle somewhat higher load just fine; the point is you genuinely don't know without testing for it.

Why D is incorrect: Passing a performance test says nothing about auto-scaling configuration or capability — those are separate, unrelated concerns.

Reinforcement: A performance test result is scoped to the load level it was run at — load testing is what tells you about the levels beyond it.

2. A system handles a gradual ramp to 2,000 req/s over ten minutes just fine, but falls over almost immediately when the same 2,000 req/s arrives as a sudden spike. What does this lesson say explains the difference?

Show answer

Correct: B

Why B is correct: This is exactly the "Under the Hood" explanation — the failure mode depends not just on the peak load reached, but on how much time the system had to prepare for it. A spike removes that preparation time entirely.

Why A is incorrect: Both scenarios describe the same system under the same peak load — only the ramp shape differs.

Why C is incorrect: Nothing about 2,000 req/s is inherently impossible — the system handled it fine under a gradual ramp, proving the load level itself wasn't the sole issue.

Why D is incorrect: Spike tests are a legitimate, deliberately different test shape specifically because they reveal real failure modes a ramp test cannot — they're not inaccurate, they're testing something different on purpose.

Reinforcement: The shape of how load arrives matters as much as the peak value itself — test both shapes when a real spike scenario is plausible.

3. A team wants to check whether their application has a slow memory leak that only becomes noticeable after several hours of sustained traffic. Which type of load test is specifically designed to catch this?

Show answer

Correct: B

Why B is correct: A soak/endurance test's entire purpose is duration — sustaining realistic load over hours specifically to surface problems, like a slow memory leak, that a short test at any load level would never reveal.

Why A is incorrect: A brief spike test is far too short in duration to reveal a slow leak — duration, not peak load, is what a leak needs to become visible.

Why C is incorrect: BenchmarkDotNet measures an isolated method's speed and allocation in a microbenchmark, not a real, whole application's memory behavior over hours of realistic traffic.

Why D is incorrect: This is explicitly called out as Mistake 2 — a short "soak test" that only runs a couple of minutes defeats the entire purpose of testing for a slow, time-dependent problem.

Reinforcement: Duration is its own dimension, independent of peak load — a soak test is the only shape built specifically to test for it.

4. A team watches only CPU usage during a load test, sees it never exceeds 40%, and concludes the system has plenty of headroom — but the system's throughput actually plateaus well before that. What does this lesson say they likely missed?

Show answer

Correct: B

Why B is correct: This is exactly Mistake 3 — a real bottleneck is very often something other than raw CPU, and watching CPU alone can miss it entirely while a different resource (a pool, GC, the database) is the actual limiting factor.

Why A is incorrect: The lesson explicitly names several other common culprits — this narrow view of CPU as the only relevant metric is exactly the mistake being illustrated.

Why C is incorrect: This scenario — low CPU, plateaued throughput — is a completely normal, common signature of a non-CPU bottleneck, not evidence of a broken test.

Why D is incorrect: This directly contradicts the lesson's point — throughput and CPU usage frequently decouple exactly when the real bottleneck lies elsewhere.

Reinforcement: Watch thread pool queue length, connection pool usage, GC frequency, and database latency alongside CPU — not CPU in isolation.

You now know how to deliberately find and characterize a system's breaking point before reality finds it for you. From here, this Part turns from controlled testing to live diagnosis — when a system actually fails under real production load, the next lessons show you exactly how to find out why.


dotnetmadeeasy.com — Learn C# and .NET, the right way.