This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Load Testing ASP.NET Core Applications with k6: Introduction
Tuesday, 02 December 2025
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:
- Part 1 (this article): Introduction to k6, installation, test types, and why k6
- Part 2: Practical Implementation: Writing tests, CI/CD integration, profiling, and real-world examples
Introduction
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:
- Install and configure k6 on any platform (Windows, Mac, Linux)
- Write comprehensive performance tests for different scenarios
- Integrate k6 into your CI/CD pipeline with GitHub Actions
- Use profiling tools (dotTrace, dotMemory) alongside k6 to find bottlenecks
- Implement different testing strategies: smoke, load, stress, spike, and soak tests
- Set up performance regression detection to prevent slow code from reaching production
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.
What is k6 and Why Use It?
k6 is a modern load testing tool built for developers. Unlike older tools like JMeter or LoadRunner, k6 is:
- Developer-friendly: Tests are written in JavaScript (ES6+)
- CLI-first: Perfect for CI/CD pipelines
- Lightweight: Single binary, no dependencies
- Accurate: Written in Go for precise metrics
- Scriptable: Full programming capabilities for complex scenarios
- Cloud-ready: Can integrate with k6 Cloud, Grafana, Prometheus
For MinimalBlog, k6 is ideal because:
- We can test caching: k6 can verify cache headers and behavior
- We can simulate real traffic: Test multiple concurrent users
- We can validate performance claims: Measure actual response times
- We can integrate with CI/CD: Automate testing in our pipeline
- We can test specific scenarios: Category filtering, individual posts, homepage
Installing k6
Windows Installation
Option 1: Using Chocolatey (Recommended)
choco install k6
Option 2: Using Winget
winget install k6 --source winget
Option 3: Manual Installation
- Download the latest Windows release from GitHub Releases
- Extract the
k6.exefile - Add the directory to your PATH or move
k6.exeto a directory already in PATH
Verify Installation:
k6 version
Mac Installation
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
Linux Installation
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
Understanding k6 Test Anatomy
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:
- Virtual Users (VUs): Simulated concurrent users
- Duration: How long the test runs
- Checks: Assertions that don't stop the test
- Thresholds: Pass/fail criteria
- Metrics: Response time, throughput, error rate
Types of Performance Tests
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
1. Smoke Tests
Purpose: Verify the system works under minimal load
When to use:
- After every code change
- Before running more intensive tests
- As a sanity check in CI/CD
Characteristics:
- 1-2 VUs (Virtual Users)
- Short duration (30s-1min)
- Tests basic functionality
2. Load Tests
Purpose: Assess performance under expected normal load
When to use:
- To establish baseline performance
- To verify SLAs are met
- Regular performance regression testing
Characteristics:
- Realistic number of users
- Sustained load
- Typical duration: 5-15 minutes
3. Stress Tests
Purpose: Find system breaking point
When to use:
- To understand capacity limits
- To identify bottlenecks
- Planning for scaling
Characteristics:
- Gradually increasing load
- Push beyond normal capacity
- Duration: 10-30 minutes
4. Spike Tests
Purpose: Test behavior under sudden traffic spikes
When to use:
- Preparing for product launches
- Testing auto-scaling
- Validating fallback behavior
Characteristics:
- Sudden large increase in load
- Short spike duration
- Total duration: 5-10 minutes
5. Soak Tests (Endurance Tests)
Purpose: Find memory leaks and degradation over time
When to use:
- Before major releases
- Testing long-running services
- Validating resource cleanup
Characteristics:
- Normal load levels
- Extended duration (hours or days)
- Monitor for degradation
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
Why k6 and Not...?
With so many load testing tools available, why should you choose k6? Let's compare k6 with popular alternatives:
k6 vs Apache JMeter
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:
- Need GUI for non-technical testers
- Testing protocols beyond HTTP (JDBC, LDAP, SOAP)
- Already have JMeter expertise in team
- Need extensive plugin ecosystem
Why k6 is better for most ASP.NET Core apps:
- Faster to write tests (JavaScript vs XML)
- Lighter resource footprint
- Better CI/CD integration
- More modern developer experience
k6 vs Locust
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:
- Python-first team
- Need free distributed testing
- Want real-time web UI during tests
- Complex Python logic in tests
Why k6 is better for most ASP.NET Core apps:
- Better performance for accurate metrics
- JavaScript more familiar for web developers
- Cleaner threshold/assertion syntax
- Better Prometheus/Grafana integration
k6 vs Gatling
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:
- JVM/Scala expertise in team
- Need stunning built-in reports
- Testing complex HTTP scenarios
- Enterprise support requirements
Why k6 is better for most ASP.NET Core apps:
- JavaScript is more accessible
- Simpler setup and execution
- Better for quick CI/CD checks
- More straightforward for simple HTTP testing
k6 vs Artillery
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:
- Prefer YAML over code
- Need Socket.io testing
- Want combined load + functional tests
- Already using Node.js ecosystem
Why k6 is better for most ASP.NET Core apps:
- More precise metrics (Go vs Node.js)
- Better threshold system
- Cleaner JavaScript API
- More mature and stable
k6 vs wrk/wrk2
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:
- Need absolute minimal overhead
- Quick one-off benchmarks
- Testing raw HTTP performance
- Low-level protocol testing
Why k6 is better for most ASP.NET Core apps:
- Much easier to write tests
- Better reporting and metrics
- Scenario support (ramp-up, stages)
- CI/CD friendly
k6 vs Playwright/Cypress (Browser-Based)
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:
- Need to test JavaScript execution
- Must verify browser rendering
- Testing complex SPAs
- Functional E2E testing
Why k6 is better for most ASP.NET Core apps:
- Protocol-level testing sufficient for server-rendered apps
- Need to simulate 100+ concurrent users
- Much more efficient resource usage
Summary: k6's Sweet Spot
Choose k6 when you want:
- Fast, accurate load testing
- Developer-friendly JavaScript API
- Excellent CI/CD integration
- Protocol-level HTTP testing (no browser needed)
- Rich scenario support (smoke, load, stress, spike, soak)
- Modern tooling with good ecosystem
- Lightweight resource usage
k6 is perfect for:
- APIs and web services
- Server-side rendered apps
- CI/CD performance gates
- Developer-driven performance testing
- Cloud-native applications
Consider alternatives if you need:
- Browser-based testing (use Playwright)
- Extensive protocol support beyond HTTP (use JMeter)
- Free distributed testing (use Locust)
- Beautiful built-in reports (use Gatling)
- GUI for non-technical testers (use JMeter)
Next Steps
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:
- Setting up your test environment
- Writing smoke, load, stress, spike, and soak tests
- Testing cache behavior
- Realistic user journey simulations
- CI/CD integration with GitHub Actions
- Performance regression detection
- Profiling with dotTrace and dotMemory
- Best practices and troubleshooting
Resources
k6 Documentation
- Official k6 docs - Complete k6 documentation
- k6 JavaScript API - All available k6 APIs
- k6 examples - Example test scripts
- k6 metrics - Understanding metrics
- k6 thresholds - Setting pass/fail criteria
Alternative Load Testing Tools
- Apache JMeter - Java-based load testing
- Locust - Python-based load testing
- Gatling - Scala-based load testing
- Artillery - Node.js load testing
- wrk - HTTP benchmarking tool
- Playwright - Browser automation & testing