Your application works perfectly on your laptop. Unit tests pass. Integration tests pass. You deploy to production, and suddenly everything grinds to a halt. Five hundred real users hit your homepage simultaneously, and your server starts returning 503 errors. Your carefully crafted caching strategy? Turns out it doesn't work quite like you thought. That database query you thought was fast? It's creating a bottleneck at scale.
This is the nightmare scenario every developer fears, but most don't test for. Performance testing isn't optional - it's the difference between a successful launch and a 3 AM emergency page. This two-part guide will show you exactly how to prevent this scenario using k6, the modern load testing tool that's powerful enough for enterprise applications but simple enough to run in your CI/CD pipeline.
This is Part 1 of a two-part series on load testing with k6:
Performance testing is critical for any production ASP.NET Core application. Whether you're building a simple blog, a complex microservice architecture, or an enterprise application, you need to know how your application behaves under load. Will it handle 100 concurrent users? 1,000? Where are the bottlenecks? Does your caching strategy actually work?
This comprehensive guide shows you how to load test ASP.NET Core applications using k6 - one of the most powerful open-source load testing tools available. While we'll use MinimalBlog as our example application (a simple markdown-based blog with memory and output caching), the techniques and patterns shown here apply to any ASP.NET Core application.
By the end of this two-part series, you'll know how to:
Why MinimalBlog as the example? It's a real ASP.NET Core 9.0 application with common patterns: Razor Pages, memory caching, output caching, file I/O, and markdown processing. The testing approaches you'll learn apply equally to your MVC apps, Web APIs, Blazor applications, or minimal APIs.
k6 is a modern load testing tool built for developers. Unlike older tools like JMeter or LoadRunner, k6 is:
For MinimalBlog, k6 is ideal because:
Option 1: Using Chocolatey (Recommended)
choco install k6
Option 2: Using Winget
winget install k6 --source winget
Option 3: Manual Installation
k6.exe filek6.exe to a directory already in PATHVerify Installation:
k6 version
Option 1: Using Homebrew (Recommended)
brew install k6
Option 2: Using MacPorts
sudo port install k6
Option 3: Manual Installation
# Download and install the latest release
curl -O -L https://github.com/grafana/k6/releases/latest/download/k6-macos-amd64.zip
unzip k6-macos-amd64.zip
sudo cp k6-macos-amd64/k6 /usr/local/bin/
sudo chmod +x /usr/local/bin/k6
Verify Installation:
k6 version
Option 1: Using Package Managers
For Debian/Ubuntu:
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
For Fedora/CentOS/RHEL:
sudo dnf install https://dl.k6.io/rpm/repo.rpm
sudo dnf install k6
Option 2: Using Snap
sudo snap install k6
Option 3: Manual Installation
# Download the latest release
curl -O -L https://github.com/grafana/k6/releases/latest/download/k6-linux-amd64.tar.gz
tar -xzf k6-linux-amd64.tar.gz
sudo cp k6-linux-amd64/k6 /usr/local/bin/
sudo chmod +x /usr/local/bin/k6
Option 4: Using Docker
docker pull grafana/k6:latest
# Run a test
docker run --rm -i grafana/k6:latest run - <script.js
Verify Installation:
k6 version
Before we dive into testing, let's understand the basic structure of a k6 test:
import http from 'k6/http';
import { check, sleep } from 'k6';
// Test configuration
export const options = {
vus: 10, // Virtual users
duration: '30s', // Test duration
};
// Setup function (runs once before test)
export function setup() {
// Prepare test data
return { baseUrl: 'http://localhost:5000' };
}
// Main test function (runs for each VU)
export default function(data) {
const response = http.get(data.baseUrl);
// Assertions
check(response, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200,
});
sleep(1); // Wait between iterations
}
// Teardown function (runs once after test)
export function teardown(data) {
// Clean up
}
Key concepts:
Before testing your application, let's understand the five main types of load tests and when to use each:
graph TD
A[Performance Testing Types] --> B[Smoke Test]
A --> C[Load Test]
A --> D[Stress Test]
A --> E[Spike Test]
A --> F[Soak Test]
B --> B1[1-2 VUs<br/>30s-1min<br/>Basic Functionality]
C --> C1[Normal Load<br/>5-15 minutes<br/>Verify SLAs]
D --> D1[Gradual Increase<br/>10-30 minutes<br/>Find Breaking Point]
E --> E1[Sudden Spike<br/>5-10 minutes<br/>Test Recovery]
F --> F1[Normal Load<br/>Hours/Days<br/>Memory Leaks]
style B stroke:#90EE90
style C stroke:#87CEEB
style D stroke:#FFD700
style E stroke:#FF6347
style F stroke:#DDA0DD
Purpose: Verify the system works under minimal load
When to use:
Characteristics:
Purpose: Assess performance under expected normal load
When to use:
Characteristics:
Purpose: Find system breaking point
When to use:
Characteristics:
Purpose: Test behavior under sudden traffic spikes
When to use:
Characteristics:
Purpose: Find memory leaks and degradation over time
When to use:
Characteristics:
graph LR
A[Start] --> B{Smoke Test Pass?}
B -->|No| C[Fix Issues]
C --> A
B -->|Yes| D{Load Test Pass?}
D -->|No| E[Optimize]
E --> D
D -->|Yes| F{Stress Test}
F --> G{Found Limit?}
G -->|Yes| H[Document Capacity]
G -->|No| I[Increase Load]
I --> F
H --> J{Spike Test Pass?}
J -->|No| K[Improve Resilience]
K --> J
J -->|Yes| L{Soak Test}
L --> M{Memory Stable?}
M -->|No| N[Fix Memory Leaks]
N --> L
M -->|Yes| O[Production Ready]
style O stroke:#90EE90
With so many load testing tools available, why should you choose k6? Let's compare k6 with popular alternatives:
Apache JMeter is the veteran of load testing tools (since 1999).
| Feature | k6 | JMeter |
|---|---|---|
| Installation | Single binary, no dependencies | Requires Java, heavier install |
| Test Definition | JavaScript code | XML or GUI |
| Resource Usage | Lightweight (Go) | Heavy (Java) |
| CI/CD Integration | Excellent (CLI-first) | Requires plugins |
| Learning Curve | Easy for developers | Steeper, GUI-focused |
| Protocol Support | HTTP, WebSockets, gRPC | Broader (FTP, JDBC, SMTP, etc.) |
When to use JMeter instead:
Why k6 is better for most ASP.NET Core apps:
Locust is a Python-based load testing tool, popular in Python shops.
| Feature | k6 | Locust |
|---|---|---|
| Language | JavaScript | Python |
| Performance | Very fast (Go runtime) | Slower (Python GIL) |
| Ease of Use | Simple JavaScript | Pythonic, easy |
| Distributed Testing | k6 Cloud (paid) | Built-in (free) |
| Web UI | Limited | Excellent real-time UI |
| Metrics Export | Many formats | CSV, web UI |
When to use Locust instead:
Why k6 is better for most ASP.NET Core apps:
Gatling is a Scala-based tool with excellent reporting.
| Feature | k6 | Gatling |
|---|---|---|
| Language | JavaScript | Scala/Java |
| Reports | Basic (+ extensions) | Beautiful built-in HTML reports |
| Learning Curve | Easy | Moderate (Scala DSL) |
| Performance | Excellent | Excellent |
| Open Source | Fully open | Open with enterprise add-ons |
| Cloud Service | k6 Cloud | Gatling Enterprise |
When to use Gatling instead:
Why k6 is better for most ASP.NET Core apps:
Artillery is another JavaScript load testing tool.
| Feature | k6 | Artillery |
|---|---|---|
| Test Definition | JavaScript code | YAML + JS hooks |
| Performance | Faster (Go) | Slower (Node.js) |
| Ease of Use | Code-based | YAML config-based |
| Built for | Load testing | Load + functional testing |
| Assertions | Excellent thresholds | Basic expectations |
| Extensibility | Extensions | Plugins |
When to use Artillery instead:
Why k6 is better for most ASP.NET Core apps:
wrk is a lightweight HTTP benchmarking tool.
| Feature | k6 | wrk |
|---|---|---|
| Ease of Use | High-level API | Low-level C + Lua |
| Scenarios | Rich scenario support | Basic HTTP only |
| Metrics | Comprehensive | Basic |
| Scripting | JavaScript | Lua |
| Use Case | Full load testing | Quick benchmarks |
When to use wrk instead:
Why k6 is better for most ASP.NET Core apps:
Playwright and Cypress are browser automation tools sometimes used for load testing.
| Feature | k6 | Playwright/Cypress |
|---|---|---|
| Approach | Protocol-level HTTP | Real browser |
| Performance | Thousands of VUs | Tens of browsers |
| Resource Usage | Lightweight | Heavy (browsers) |
| JavaScript Execution | No | Yes |
| Primary Use Case | Load testing | E2E functional testing |
When to use Playwright/Cypress instead:
Why k6 is better for most ASP.NET Core apps:
Choose k6 when you want:
k6 is perfect for:
Consider alternatives if you need:
Now that you understand what k6 is, how to install it, the types of performance tests available, and why k6 is a great choice for ASP.NET Core applications, you're ready to start writing actual tests.
Continue to Part 2: Practical Implementation where we'll cover:
© 2025 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.