Building a functional API is only the first step toward a successful application. As user traffic grows, APIs must continue to deliver consistent response times, maintain reliability under load, and provide enough operational visibility to identify performance bottlenecks. Without realistic load testing, performance issues often remain hidden until they affect production users.
k6 is an open-source load testing tool that allows developers to simulate realistic traffic patterns, while OpenTelemetry provides standardized observability through traces, metrics, and logs. Together, they help development teams measure API performance and understand why bottlenecks occur.
In this article, you'll learn how to load test ASP.NET Core APIs using k6, integrate OpenTelemetry for observability, and apply production-ready performance testing practices.
Why Load Testing Matters
Functional testing confirms that an API works correctly.
Load testing answers different questions:
How many concurrent users can the API handle?
Does latency remain consistent under heavy traffic?
Which component becomes the bottleneck?
Does the application recover after traffic spikes?
Are database queries scaling efficiently?
Without load testing, these questions remain unanswered until real users encounter problems.
Performance Testing Types
Different tests serve different purposes.
| Test Type | Purpose |
|---|
| Load Testing | Measure expected workload |
| Stress Testing | Determine breaking point |
| Spike Testing | Evaluate sudden traffic increases |
| Soak Testing | Measure long-term stability |
| Scalability Testing | Evaluate growth behavior |
Most production systems benefit from using more than one testing strategy.
Test Architecture
A typical performance testing environment looks like this:
k6
|
HTTP Requests
|
Load Balancer
|
ASP.NET Core API
|
Database
|
OpenTelemetry
|
Monitoring Dashboard
OpenTelemetry captures telemetry while k6 generates realistic traffic.
Creating a Sample API
Create a new ASP.NET Core project.
dotnet new webapi -n LoadTestApi
Example endpoint:
app.MapGet("/products", () =>
{
return Results.Ok(new[]
{
"Laptop",
"Keyboard",
"Monitor"
});
});
This endpoint provides a simple starting point for load testing.
Installing k6
Download and install k6 according to your operating system.
Verify the installation.
k6 version
Once installed, you can execute JavaScript-based performance scripts.
Writing a Load Test
Create a file named script.js.
import http from "k6/http";
export default function ()
{
http.get(
"http://localhost:5000/products");
}
Execute the test.
k6 run script.js
This sends repeated requests to the API and collects performance metrics.
Simulating Concurrent Users
Increase the virtual user count.
export const options =
{
vus: 50,
duration: "30s"
};
This configuration simulates 50 concurrent users for 30 seconds.
Adjust values based on your expected production workload.
Configuring OpenTelemetry
Enable tracing.
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing.AddAspNetCoreInstrumentation();
tracing.AddHttpClientInstrumentation();
});
Enable metrics.
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation();
});
Instrumentation allows request flow to be observed during load tests.
Understanding Distributed Tracing
A request may travel through multiple services.
Client
|
API Gateway
|
ASP.NET Core API
|
Database
Distributed tracing links every step using a shared trace identifier, making bottlenecks easier to identify.
Key Performance Metrics
During testing, monitor:
| Metric | Why It Matters |
|---|
| Average Latency | Overall response time |
| P95 Latency | User experience under load |
| P99 Latency | Worst-case performance |
| Requests per Second | Throughput |
| Error Rate | Reliability |
| CPU Usage | Resource utilization |
| Memory Usage | Capacity planning |
These metrics provide a balanced view of API performance.
Measuring Database Performance
Application performance often depends on database efficiency.
Monitor:
Query duration
Connection pool usage
Lock contention
Index utilization
Active connections
Slow database operations frequently become the primary bottleneck during load testing.
Adding Custom Metrics
Applications can expose additional metrics.
Example:
logger.LogInformation(
"Processing order {Id}",
orderId);
Useful custom metrics include:
Orders processed
Cache hits
External API calls
Background jobs executed
Business metrics complement infrastructure metrics.
Testing API Scalability
Increase traffic gradually.
10 Users
|
50 Users
|
100 Users
|
250 Users
Gradual scaling helps identify the point where performance begins to degrade.
Avoid increasing traffic too aggressively without monitoring resource utilization.
Identifying Bottlenecks
Performance issues often originate from:
Database queries
External APIs
File storage
Network latency
Thread pool exhaustion
Lock contention
OpenTelemetry traces help pinpoint which component contributes most to request latency.
Error Analysis
Monitor:
HTTP 500 responses
Request timeouts
Connection failures
Retry attempts
Dependency failures
Even low error rates can become significant under sustained traffic.
Production Best Practices
| Practice | Benefit |
|---|
| Test production-like workloads | More accurate results |
| Measure latency percentiles | Better user experience analysis |
| Monitor infrastructure metrics | Identify bottlenecks |
| Enable distributed tracing | Easier debugging |
| Increase traffic gradually | Safer testing |
| Test realistic user behavior | Better performance insights |
| Repeat benchmarks regularly | Track performance changes |
Common Mistakes
| Mistake | Better Approach |
|---|
| Testing only one endpoint | Cover critical workflows |
| Measuring average latency only | Include P95 and P99 metrics |
| Ignoring infrastructure monitoring | Collect CPU and memory metrics |
| Testing against development environments | Use production-like configurations |
| Running one benchmark only | Repeat tests for consistency |
| Optimizing without baseline measurements | Establish reference metrics first |
Troubleshooting
High response latency
Review:
Increasing error rates
Check:
Connection pool limits
Resource exhaustion
Timeout configuration
Dependency availability
CPU reaches maximum utilization
Investigate:
Expensive business logic
Serialization overhead
Background processing
Thread contention
Memory usage continues growing
Inspect:
k6 and OpenTelemetry Together
| Capability | k6 | OpenTelemetry |
|---|
| Generate Load | Yes | No |
| Measure Latency | Yes | Yes |
| Distributed Tracing | No | Yes |
| Infrastructure Metrics | Limited | Yes |
| Request Simulation | Yes | No |
| Root Cause Analysis | Limited | Excellent |
k6 generates realistic workloads, while OpenTelemetry explains how the application behaves under those workloads.
Frequently Asked Questions
Why use k6 instead of sending requests manually?
Manual testing cannot simulate realistic concurrent traffic. k6 allows developers to model production-like workloads and collect detailed performance metrics.
Why are P95 and P99 latency important?
Average latency may hide slow requests. Percentile metrics reveal how the application performs under less favorable conditions and better represent the user experience.
Does OpenTelemetry replace load testing?
No. OpenTelemetry provides observability, while k6 generates the traffic needed to evaluate application performance.
How often should load testing be performed?
Load testing should be included before major releases, after infrastructure changes, and whenever significant application updates affect performance.
Can load testing identify database problems?
Yes. High traffic often exposes inefficient queries, indexing issues, connection pool limitations, and lock contention that may not appear during functional testing.
Conclusion
Reliable APIs require more than functional correctness—they must continue to perform predictably under real-world traffic conditions. Load testing with k6 helps simulate production workloads, while OpenTelemetry provides the visibility needed to understand how requests flow through the application and where performance bottlenecks occur.
By combining realistic traffic generation, distributed tracing, infrastructure monitoring, and regular benchmarking, development teams can build ASP.NET Core APIs that are scalable, resilient, and ready to support high-volume production environments. Continuous performance testing should become a standard part of the software delivery lifecycle rather than an activity reserved for release day.