
Introduction
ASP.NET applications power everything from internal line-of-business portals to large-scale public APIs and e-commerce platforms. Whether you’re running ASP.NET MVC, ASP.NET Core, Razor Pages, or a Web API backend, load testing is essential to understand how your application behaves under real user traffic.
An ASP.NET load testing strategy helps you answer critical questions:
- How many concurrent users can your application support?
- Which endpoints become slow first under load?
- Does authentication create a bottleneck?
- How does your app behave when traffic spikes suddenly?
- Are database queries, session state, caching, or external dependencies limiting performance?
With LoadForge, you can run cloud-based load testing against your ASP.NET application using Locust-based Python scripts. That means you get flexible scripting, distributed testing, real-time reporting, CI/CD integration, and global test locations to simulate realistic user traffic from around the world.
In this guide, you’ll learn how to create practical ASP.NET performance testing scripts, model realistic user behavior, and analyze the results to improve scalability before peak traffic hits.
Prerequisites
Before you begin load testing your ASP.NET application with LoadForge, make sure you have:
- A deployed ASP.NET or ASP.NET Core application in a test, staging, or production-like environment
- The base URL of the application, such as
https://staging.contoso-shop.com - Test user accounts for authenticated scenarios
- Knowledge of your key application workflows, such as login, product browsing, checkout, dashboard usage, or API consumption
- Permission to generate traffic against the target environment
- LoadForge account access to run distributed load tests
It also helps to have:
- Application Performance Monitoring (APM) tools such as Application Insights, New Relic, or Datadog
- Database monitoring enabled for SQL Server
- ASP.NET logs available for correlation
- Baseline performance expectations, such as acceptable response times and throughput
For ASP.NET specifically, you should identify whether your application uses:
- Cookie-based authentication with ASP.NET Identity
- JWT bearer tokens for APIs
- Anti-forgery tokens on form posts
- Session state or distributed caching
- Entity Framework Core for data access
- External services such as Redis, SQL Server, Azure Service Bus, or third-party APIs
These details matter because they influence how your application behaves under load and how your Locust scripts should be written.
Understanding ASP.NET Under Load
ASP.NET applications are generally highly capable, but performance issues often appear when concurrency increases. Load testing helps expose those issues before users do.
Common ASP.NET bottlenecks
When an ASP.NET application is under load, the most common bottlenecks include:
Database contention
Many ASP.NET applications rely heavily on SQL Server through Entity Framework or Dapper. Under load, slow queries, lock contention, missing indexes, and connection pool exhaustion can dramatically increase response times.
Authentication overhead
ASP.NET Identity login flows often involve database lookups, password hashing, claims generation, and cookie issuance. API authentication may require token validation on every request. These steps can become expensive at scale.
Session and state management
If your application uses in-memory session state, horizontal scaling can become difficult. If it uses SQL-backed or Redis-backed session state, the session provider itself may become a bottleneck.
View rendering and serialization
Razor view rendering, JSON serialization, and model binding can consume CPU under high concurrency, especially when large payloads are returned.
Thread pool starvation
ASP.NET Core is optimized for async I/O, but blocking code, synchronous database calls, or slow external services can starve the thread pool and cause cascading latency.
Caching inefficiencies
If frequently accessed pages or API responses are not cached effectively, your application may repeatedly hit the database or backend services for the same data.
File uploads and large request bodies
Applications that accept document uploads, image uploads, or report generation requests may consume memory, disk I/O, and CPU quickly under stress.
A good ASP.NET performance testing plan should include:
- Anonymous traffic
- Authenticated user sessions
- Read-heavy and write-heavy workflows
- API and UI endpoints
- Peak traffic spikes
- Sustained load and stress testing
Writing Your First Load Test
Let’s start with a realistic basic load test for an ASP.NET Core e-commerce application. This script simulates anonymous users browsing the home page, category pages, product detail pages, and search.
These are typical endpoints you might see in an ASP.NET MVC or Razor Pages app:
//Products/Category/electronics/Products/Details/1042/Search?q=laptop
from locust import HttpUser, task, between
import random
class AspNetAnonymousUser(HttpUser):
wait_time = between(1, 3)
product_ids = [1001, 1002, 1015, 1042, 1058, 1071]
categories = ["electronics", "books", "office", "gaming", "accessories"]
search_terms = ["laptop", "wireless mouse", "monitor", "keyboard", "headphones"]
@task(3)
def home_page(self):
self.client.get("/", name="GET /")
@task(4)
def browse_category(self):
category = random.choice(self.categories)
self.client.get(f"/Products/Category/{category}", name="GET /Products/Category/[category]")
@task(5)
def product_details(self):
product_id = random.choice(self.product_ids)
self.client.get(f"/Products/Details/{product_id}", name="GET /Products/Details/[id]")
@task(2)
def search_products(self):
term = random.choice(self.search_terms)
self.client.get(f"/Search?q={term}", name="GET /Search")What this script does
This first test models lightweight browsing traffic, which is often the majority of traffic for public ASP.NET applications. It helps you measure:
- Static and dynamic page response times
- Routing and middleware overhead
- Razor rendering performance
- Database query performance for product and search pages
- Cache effectiveness for common pages
Why this matters for ASP.NET
ASP.NET applications often appear healthy at low traffic but struggle when product detail pages and search endpoints trigger repeated database queries. This script is a strong starting point for identifying:
- Slow controller actions
- Inefficient Entity Framework queries
- Missing output caching
- Excessive view model mapping
- Search endpoint performance issues
In LoadForge, you can scale this script across multiple cloud load generators to simulate realistic traffic volumes and observe response times in real time.
Advanced Load Testing Scenarios
Once basic browsing is covered, the next step is to simulate realistic authenticated workflows. For ASP.NET applications, that usually means login, dashboard access, API calls, form submissions, and transactional flows.
Scenario 1: Testing ASP.NET Identity login and authenticated account usage
Many ASP.NET applications use cookie-based authentication with ASP.NET Identity. The following script logs in through a typical MVC account endpoint and then accesses authenticated pages such as account profile, order history, and saved addresses.
from locust import HttpUser, task, between
from bs4 import BeautifulSoup
import random
class AspNetAuthenticatedUser(HttpUser):
wait_time = between(2, 5)
credentials = [
{"email": "loadtest.user1@contoso-shop.com", "password": "P@ssw0rd123!"},
{"email": "loadtest.user2@contoso-shop.com", "password": "P@ssw0rd123!"},
{"email": "loadtest.user3@contoso-shop.com", "password": "P@ssw0rd123!"},
]
def on_start(self):
self.login()
def login(self):
creds = random.choice(self.credentials)
login_page = self.client.get("/Account/Login", name="GET /Account/Login")
soup = BeautifulSoup(login_page.text, "html.parser")
token_input = soup.find("input", {"name": "__RequestVerificationToken"})
verification_token = token_input["value"] if token_input else ""
payload = {
"Email": creds["email"],
"Password": creds["password"],
"RememberMe": "false",
"__RequestVerificationToken": verification_token
}
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
with self.client.post(
"/Account/Login",
data=payload,
headers=headers,
allow_redirects=True,
name="POST /Account/Login",
catch_response=True
) as response:
if "My Account" in response.text or response.url.endswith("/Account/Manage"):
response.success()
else:
response.failure("Login failed")
@task(3)
def account_dashboard(self):
self.client.get("/Account/Manage", name="GET /Account/Manage")
@task(2)
def order_history(self):
self.client.get("/Account/Orders", name="GET /Account/Orders")
@task(1)
def saved_addresses(self):
self.client.get("/Account/Addresses", name="GET /Account/Addresses")Why this test is important
Authenticated traffic is often much more expensive than anonymous traffic. In ASP.NET, login and account pages may involve:
- ASP.NET Identity database lookups
- Claims generation
- Authentication cookie creation and validation
- Personalized data queries
- Session and cache usage
This test can uncover:
- Slow login performance
- High latency on account pages
- Database bottlenecks in user-specific queries
- Session storage issues
- Authentication middleware overhead
If your application uses anti-forgery tokens, as many ASP.NET form-based applications do, extracting and submitting the token is necessary for realistic testing.
Scenario 2: Load testing an ASP.NET Core Web API with JWT authentication
Many modern ASP.NET Core applications expose REST APIs for SPAs, mobile apps, and third-party integrations. This example simulates a client authenticating against a token endpoint and calling protected API routes.
Example endpoints:
POST /api/auth/loginGET /api/ordersGET /api/orders/84721POST /api/ordersGET /api/products?category=electronics&page=1
from locust import HttpUser, task, between
import random
class AspNetApiUser(HttpUser):
wait_time = between(1, 2)
users = [
{"email": "apiuser1@contoso-shop.com", "password": "ApiP@ss123!"},
{"email": "apiuser2@contoso-shop.com", "password": "ApiP@ss123!"},
]
product_ids = [1001, 1002, 1015, 1042]
order_ids = [84721, 84722, 84723]
def on_start(self):
self.token = None
self.authenticate()
def authenticate(self):
creds = random.choice(self.users)
payload = {
"email": creds["email"],
"password": creds["password"]
}
with self.client.post(
"/api/auth/login",
json=payload,
name="POST /api/auth/login",
catch_response=True
) as response:
if response.status_code == 200 and "token" in response.json():
self.token = response.json()["token"]
response.success()
else:
response.failure(f"Authentication failed: {response.text}")
def auth_headers(self):
return {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
@task(4)
def list_products(self):
category = random.choice(["electronics", "books", "office"])
page = random.randint(1, 5)
self.client.get(
f"/api/products?category={category}&page={page}",
headers=self.auth_headers(),
name="GET /api/products"
)
@task(3)
def list_orders(self):
self.client.get(
"/api/orders",
headers=self.auth_headers(),
name="GET /api/orders"
)
@task(2)
def get_order_details(self):
order_id = random.choice(self.order_ids)
self.client.get(
f"/api/orders/{order_id}",
headers=self.auth_headers(),
name="GET /api/orders/[id]"
)
@task(1)
def create_order(self):
payload = {
"customerId": 12045,
"shippingAddressId": 501,
"items": [
{"productId": 1001, "quantity": 1, "unitPrice": 1299.99},
{"productId": 1042, "quantity": 2, "unitPrice": 49.99}
],
"paymentMethod": "CreditCard",
"currency": "USD"
}
self.client.post(
"/api/orders",
json=payload,
headers=self.auth_headers(),
name="POST /api/orders"
)What this API load test reveals
This script is useful for ASP.NET Core API performance testing because it covers both read and write operations. It can reveal:
- JWT authentication overhead
- API serialization/deserialization costs
- Database performance for order retrieval
- Write latency for transactional operations
- Locking or contention during order creation
- Throughput limits on protected endpoints
This is especially valuable if your ASP.NET backend serves a React, Angular, Blazor, or mobile frontend.
Scenario 3: Testing file uploads and report generation in ASP.NET
A common real-world ASP.NET use case is uploading files such as invoices, profile images, or CSV imports. These operations are much heavier than standard page loads and should be tested separately.
This example simulates an authenticated user uploading a CSV file to an ASP.NET Core admin endpoint and then checking job status.
from locust import HttpUser, task, between
import io
import random
class AspNetFileUploadUser(HttpUser):
wait_time = between(3, 6)
def on_start(self):
self.token = None
self.authenticate()
def authenticate(self):
payload = {
"email": "admin.loadtest@contoso-shop.com",
"password": "AdminP@ss123!"
}
response = self.client.post("/api/auth/login", json=payload, name="POST /api/auth/login")
if response.status_code == 200:
self.token = response.json().get("token")
def auth_headers(self):
return {
"Authorization": f"Bearer {self.token}"
}
@task(2)
def upload_inventory_file(self):
csv_content = """sku,name,price,quantity,category
LT-1001,Contoso Laptop 15,1299.99,25,electronics
MS-2042,Wireless Mouse,49.99,100,accessories
KB-3055,Mechanical Keyboard,89.99,75,accessories
MN-4401,27 Inch Monitor,299.99,40,electronics
"""
file_data = io.BytesIO(csv_content.encode("utf-8"))
files = {
"file": ("inventory-update.csv", file_data, "text/csv")
}
with self.client.post(
"/api/admin/inventory/import",
headers=self.auth_headers(),
files=files,
name="POST /api/admin/inventory/import",
catch_response=True
) as response:
if response.status_code in [200, 202]:
response.success()
else:
response.failure(f"Upload failed: {response.status_code}")
@task(1)
def check_import_status(self):
job_id = random.choice([3011, 3012, 3013])
self.client.get(
f"/api/admin/inventory/import/{job_id}/status",
headers=self.auth_headers(),
name="GET /api/admin/inventory/import/[jobId]/status"
)Why file upload testing matters
File uploads and background processing often stress different parts of the ASP.NET stack:
- Request buffering and body parsing
- File system or blob storage I/O
- Antivirus or validation checks
- Background job queues
- Database writes for import records
- Memory pressure from large payloads
These scenarios are ideal for stress testing because they can expose CPU spikes, memory issues, and long-running request handling problems that basic page browsing won’t reveal.
Analyzing Your Results
After running your ASP.NET load testing scenarios in LoadForge, focus on the metrics that matter most.
Response time percentiles
Average response time can hide serious issues. Instead, pay attention to:
- P50 for typical user experience
- P95 for degraded but common slow requests
- P99 for worst-case user experience
For example, if /api/orders has a 300 ms average but a 4-second P95, that usually indicates intermittent contention, slow queries, or dependency issues.
Requests per second
Throughput tells you how much traffic your ASP.NET application can sustain. Compare throughput against:
- CPU utilization
- SQL Server DTU or CPU usage
- Memory consumption
- Thread pool activity
- Error rates
If throughput plateaus while response times climb, you’ve likely hit a bottleneck.
Error rates
Watch for:
500 Internal Server Error502/503/504from proxies or load balancers401/403in auth flows429 Too Many Requests- Request timeouts
In ASP.NET applications, spikes in 500 errors during load often point to database failures, unhandled exceptions, thread starvation, or dependency timeouts.
Endpoint-level analysis
Break results down by route:
GET /Products/Details/[id]POST /Account/LoginGET /api/ordersPOST /api/admin/inventory/import
This helps you isolate which controller actions or API endpoints are failing first under load.
Correlate with ASP.NET telemetry
Use LoadForge’s real-time reporting alongside your monitoring tools to correlate:
- Slow requests with SQL query duration
- Error spikes with application logs
- Throughput changes with CPU and memory trends
- Authentication latency with identity store performance
Because LoadForge supports distributed testing, you can also identify whether latency differs by geographic region or traffic source.
Performance Optimization Tips
Once your ASP.NET performance testing identifies weak spots, these are some of the most effective optimizations.
Optimize database access
- Add indexes for frequently filtered columns
- Review slow Entity Framework queries
- Avoid N+1 query patterns
- Use projection to fetch only needed fields
- Cache frequently requested reference data
Use async correctly
ASP.NET Core performs best when I/O is asynchronous. Make sure database calls, HTTP calls, and file operations use async APIs rather than blocking threads.
Cache aggressively where appropriate
- Use response caching for public content
- Cache expensive queries in Redis or memory
- Cache product catalogs, category lists, and configuration data
- Reduce repeated authentication-related lookups
Reduce payload size
Large HTML pages and JSON responses increase serialization time and network transfer. Consider:
- Pagination
- Response compression
- Smaller DTOs
- Avoiding over-fetching
Tune authentication flows
- Reduce unnecessary claims
- Cache token validation metadata
- Optimize user profile queries
- Consider sliding expiration carefully
Offload heavy work
For report generation, imports, exports, and image processing:
- Move long-running work to background jobs
- Return
202 Acceptedwhere appropriate - Use queues and worker services
Scale strategically
If your ASP.NET application is already optimized, use LoadForge to validate horizontal scaling with:
- Multiple app instances
- Distributed caching
- Database read replicas
- CDN for static assets
Common Pitfalls to Avoid
ASP.NET load testing is most useful when it reflects real traffic patterns. Avoid these common mistakes.
Testing only the home page
A homepage-only test misses the expensive parts of your application. Include authenticated workflows, API calls, searches, writes, and file operations.
Ignoring anti-forgery tokens
Many ASP.NET forms require __RequestVerificationToken. If your script doesn’t handle it correctly, requests may fail or behave differently than real users.
Using unrealistic user behavior
Real users don’t hammer the same endpoint continuously with no think time. Add realistic wait times and varied workflows.
Skipping authentication
Authentication can be one of the most expensive parts of an ASP.NET application. If you don’t test login and protected routes, you may miss critical bottlenecks.
Running tests against non-production-like environments
A tiny staging database or underpowered server can produce misleading results. Test against an environment that resembles production as closely as possible.
Not monitoring backend dependencies
If SQL Server, Redis, or external APIs are the true bottleneck, HTTP response data alone won’t tell the whole story.
Failing to separate load testing from stress testing
Load testing checks expected traffic levels. Stress testing pushes beyond normal capacity to find failure points. Both matter, but they answer different questions.
Forgetting test data management
Transactional ASP.NET tests often create orders, uploads, or user activity. Make sure your environment can handle repeated test execution without polluted or conflicting data.
Conclusion
ASP.NET applications can perform extremely well, but only if you understand how they behave under realistic traffic. With the right load testing approach, you can uncover slow controller actions, authentication bottlenecks, database contention, and scaling limitations before they affect users.
Using LoadForge, you can build realistic Locust-based scripts for ASP.NET MVC apps, Razor Pages, and ASP.NET Core APIs, then run them with distributed cloud infrastructure, real-time reporting, global test locations, and CI/CD integration. That makes it much easier to validate performance before a release, during peak season, or as part of ongoing performance testing and stress testing.
If you’re ready to see how your ASP.NET application handles real-world traffic, try LoadForge and start building your first load test today.
LoadForge Team
LoadForge is a load and performance testing platform built on Locust. Our team has been shipping load tests against production systems since 2018, and we write these guides from real customer engagements.
Related guides
Keep going with more guides from the same category.

CakePHP Load Testing Guide with LoadForge
Load test CakePHP applications with LoadForge to benchmark app performance, simulate traffic, and improve scalability.

Django Load Testing Guide with LoadForge
Discover how to load test Django applications with LoadForge to measure performance, handle traffic spikes, and improve stability.

Express.js Load Testing Guide with LoadForge
Run realistic load tests for Express.js apps with LoadForge to identify bottlenecks and improve API and web app performance.