LoadForge LogoLoadForge

ASP.NET Load Testing Guide with LoadForge

ASP.NET Load Testing Guide with LoadForge

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
python
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.

python
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/login
  • GET /api/orders
  • GET /api/orders/84721
  • POST /api/orders
  • GET /api/products?category=electronics&page=1
python
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.

python
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 Error
  • 502/503/504 from proxies or load balancers
  • 401/403 in auth flows
  • 429 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/Login
  • GET /api/orders
  • POST /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 Accepted where 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.

Try LoadForge free for 7 days

Set up your first load test in under 2 minutes. No commitment.