Even with typical CPU and memory use, an ASP.NET Core API may nevertheless feel sluggish. Troubleshooting that is one of the more annoying issues with API performance. You examine the server. CPU appears to be in good condition. Memory appears to be in good condition. The database is accessible. Infrastructure breakdowns are not readily apparent.
However, an endpoint that ought to react within 100–200 ms is taking 1-2 seconds.
The explanation is that CPU and memory are not the only factors that affect API latency.
A request can spend most of its time waiting for a database query, another API, a thread, a lock, serialization, or some application code that is doing more work than expected.
The first question I ask when troubleshooting a slow ASP.NET Core API is:
Where is the request actually spending its time?
This article walks through 10 common ASP.NET Core performance bottlenecks and, more importantly, how to identify them before changing the code.
1. Start With Measurement, Not Optimization
Before changing a LINQ query, adding caching, or increasing server resources, establish where the latency comes from.
A useful way to think about an API request is:
Total Request Time
|
+-- Application code
+-- Database
+-- External APIs
+-- Serialization
+-- Network
+-- Middleware
+-- Thread scheduling / blocking
For example, suppose an endpoint takes 1.4 seconds:
Application code 120 ms
SQL queries 180 ms
External API 900 ms
JSON serialization 80 ms
Other 120 ms
--------------------------------
Total 1400 ms
Optimizing the 180 ms database operation will not solve the main problem.
The external API is responsible for most of the latency.
This is why performance troubleshooting should generally follow:
Measure → Trace → Identify → Fix → Measure again
Useful metrics include:
- Average response time
- P95 latency
- P99 latency
- Requests per second
- Database query duration
- Number of database calls
- External dependency latency
- Exception rate
- Thread-pool behavior
- Response payload size
Average latency is useful, but P95 and P99 can reveal problems that average measurements hide.
2. Slow SQL Queries
The database is one of the first places to investigate when an ASP.NET Core API becomes slow.
A query that performs well with 10,000 rows in development may behave very differently when the production database contains millions of records.
Common causes include:
- Missing indexes
- Inefficient joins
- Full table scans
- Large result sets
- Selecting unnecessary columns
- Poorly written LINQ queries
- Repeated database calls
- Expensive execution plans
For example:
var customers = await db.Customers
.Where(x => x.Status == "Active")
.ToListAsync();
The LINQ itself doesn't tell you whether the generated SQL is efficient.
You need to inspect the SQL and execution plan.
With EF Core, you can inspect generated SQL during development:
var query = db.Customers
.Where(x => x.Status == "Active");
Console.WriteLine(query.ToQueryString());
Then check the actual database behavior.
Look at:
- Execution plan
- Index usage
- Query duration
- Number of rows scanned
- Number of rows returned
- Database round trips
A useful rule is:
Don't optimize the C# code around a slow query until you know how much time the database is actually consuming.
3. EF Core N+1 Queries
The N+1 query problem is particularly easy to introduce when working with Entity Framework Core.
Imagine an endpoint retrieves 100 customers:
1 query → Get 100 customers
100 queries → Get details for each customer
Total = 101 database queries
The API may work correctly, but the number of database round trips can quickly become a performance problem.
A simplified example looks like this:
var customers = await db.Customers
.ToListAsync();
foreach (var customer in customers)
{
var orders = await db.Orders
.Where(x => x.CustomerId == customer.Id)
.ToListAsync();
}
With 100 customers, this can result in 101 database calls.
One approach is to project only the data required by the API:
var customers = await db.Customers
.Select(c => new CustomerDto
{
Id = c.Id,
Name = c.Name,
Email = c.Email
})
.ToListAsync();
Depending on the relationship and use case, eager loading, projections, joins, or specifically designed queries may be appropriate.
The important part is not simply changing the LINQ.
Measure the generated SQL and verify how many database calls the endpoint actually makes.
4. Blocking Calls and Thread Pool Starvation
ASP.NET Core is designed around asynchronous I/O, but blocking calls can still appear in application code.
For example:
var result = service.GetDataAsync().Result;
or:
service.GetDataAsync().Wait();
These calls can block a thread while waiting for I/O.
Under light traffic, the problem may not be obvious.
Under higher concurrency, blocked threads can accumulate and contribute to thread-pool starvation.
Possible symptoms include:
- Increasing response times
- Requests waiting in queues
- Reduced throughput
- Timeouts
- Increasing thread counts
- CPU usage that does not necessarily look high
Prefer asynchronous code throughout the request path:
var result = await service.GetDataAsync();
Also look for:
.Result
.Wait()
Blocking locks
Synchronous I/O
Blocking third-party libraries
For runtime investigation, tools such as dotnet-counters and dotnet-trace can help identify thread-pool and runtime behavior.
The important point is that low CPU utilization does not automatically mean the application is healthy.
An application can be waiting rather than computing.
5. Slow External API Calls
Your ASP.NET Core application may be fast while one of its dependencies is slow.
Consider an endpoint that calls three services:
Customer API 150 ms
Payment API 300 ms
Shipping API 800 ms
--------------------------------
Total 1,250 ms
Your own application code may execute in only a small fraction of that time.
The user still waits 1.25 seconds.
This is why distributed tracing becomes important in applications with multiple dependencies.
For outbound HTTP calls, consider:
IHttpClientFactory
- Async HTTP calls
- Connection reuse
- Appropriate timeouts
- Limited retries
- Circuit breakers
- Caching where appropriate
- Distributed tracing
For example:
public async Task<Customer> GetCustomerAsync(
int customerId,
CancellationToken cancellationToken)
{
return await httpClient.GetFromJsonAsync<Customer>(
$"customers/{customerId}",
cancellationToken);
}
Also be careful with retries.
A retry strategy that blindly retries every failure can increase traffic against an already unhealthy dependency.
6. Large API Responses and JSON Serialization
Sometimes the API isn't slow because it is doing too much work.
It is slow because it is returning too much data.
An endpoint that originally returned a 20 KB response may eventually return hundreds of KB—or several MB—as new fields and relationships are added.
Large payloads affect:
- JSON serialization
- Network transfer
- Memory usage
- Client-side processing
- Mobile application performance
Instead of returning an entire entity:
return Ok(customer);
consider using a DTO designed specifically for the endpoint:
var result = await db.Customers
.Select(c => new CustomerResponse
{
Id = c.Id,
Name = c.Name,
Email = c.Email
})
.ToListAsync();
Other useful techniques include:
- Pagination
- Filtering
- Sorting
- Field selection
- Compression where appropriate
- Avoiding unnecessary nested objects
A smaller response is often easier to process at every layer.
7. Poor Caching Strategy
Caching can reduce database calls and repeated computation, but adding a cache does not automatically make an API faster.
First identify whether the data is a good candidate for caching.
For example:
Frequently requested
+
Expensive to calculate
+
Doesn't change frequently
=
Good caching candidate
Depending on the application, you may use:
- IMemoryCache
- Distributed caching
- Response caching
- Redis
- Application-level caching
But caching introduces its own questions:
- How long should the data remain cached?
- When should it be invalidated?
- Is stale data acceptable?
- Is the data sensitive?
- Does every application instance need the same cached value?
A poorly designed cache can create consistency problems or add unnecessary infrastructure.
Cache deliberately, not automatically.
8. Database and HTTP Connection Management
Connection management problems can remain hidden during development and become visible under production traffic.
One common HTTP mistake is repeatedly creating HttpClient instances instead of using IHttpClientFactory.
For example, avoid creating clients unnecessarily for every request:
using var client = new HttpClient();
For ASP.NET Core applications, IHttpClientFactory provides a better approach to managing HTTP clients and handlers.
Example:
builder.Services.AddHttpClient<PaymentClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.Timeout = TimeSpan.FromSeconds(10);
});
Connection management can affect:
- Connection reuse
- Socket usage
- Connection establishment time
- API latency
- Reliability under load
The same principle applies to database connections.
Use the connection pooling and lifecycle management provided by your database provider and ORM rather than manually creating unnecessary connections.
9. Excessive Middleware and Logging
Logging is essential for production troubleshooting.
But logging everything on every request can become expensive.
For example, logging large request and response objects can involve:
Build log message
↓
Serialize object
↓
Write log
↓
Send to logging platform
↓
Store and process
When an API handles thousands of requests per minute, that additional work can become noticeable.
Review:
- Log levels
- Request/response logging
- Large object logging
- Duplicate logs
- Middleware that runs on every request
The goal isn't to remove logging.
The goal is to make logging useful without adding unnecessary work to the request path.
The same applies to middleware.
If every request passes through many expensive middleware components, the overhead can accumulate.
Measure the middleware pipeline before removing components blindly.
10. Inefficient Application Logic
Not every slow ASP.NET Core API has a database problem.
Sometimes the bottleneck is simply application code.
Common examples include:
- Expensive loops
- Repeated LINQ operations
- Processing large collections in memory
- Repeated calculations
- Excessive object allocation
- Unnecessary transformations
- Performing the same operation multiple times
Consider this example:
Database query 50 ms
Application logic 600 ms
Serialization 50 ms
---------------------------
Total 700 ms
Optimizing the SQL query from 50 ms to 30 ms will barely change the overall response time.
The application logic is where the majority of the time is being spent.
This is where profiling becomes useful.
Instead of guessing which method is slow, use profiling and tracing to identify the actual hot path.
How to Troubleshoot a Slow ASP.NET Core API?
When an endpoint becomes slow, I recommend following a structured process rather than changing multiple things at once.
Step 1: Establish the baseline
Record:
- Average latency
- P95 latency
- P99 latency
- Requests/sec
- Error rate
Step 2: Break Down the Request
Measure:
- API processing
- Database
- External APIs
- Serialization
- Network
Step 3: Check Database Activity
Look for:
- Slow queries
- Missing indexes
- Large result sets
- N+1 queries
- Repeated database calls
Step 4: Check Asynchronous Code
Search the codebase for:
.Result
.Wait()
Blocking locks
Synchronous I/O
Step 5: Trace External Dependencies
Find out whether the API is waiting on:
Payment services
Authentication providers
CRMs
Third-party APIs
Internal microservices
Step 6: Inspect Response Size
Check whether the endpoint is returning more data than the client actually needs.
Step 7: Profile Application Code
If the database and dependencies look healthy, profile the application itself.
Step 8: Fix One Bottleneck
Don't make five performance changes at once.
Change one thing, measure the result, and continue from there.
A Simple Performance Investigation Example
Suppose an endpoint has this profile:
Request latency: 1,850 ms
SQL queries: 620 ms
External API: 900 ms
Application code: 180 ms
Serialization: 80 ms
Other: 70 ms
It would be tempting to start optimizing the C# application code.
But the measurements tell us something different.
The two largest contributors are:
External API 900 ms
Database 620 ms
Those are the first areas worth investigating.
Now imagine the database profile reveals:
1 query
+
100 related queries
=
101 database calls
Fixing the N+1 problem could have a much larger impact than micro-optimizing a LINQ expression elsewhere in the application.
This is why profiling beats guessing.
Final Takeaway
A slow ASP.NET Core API does not necessarily mean you need a larger server or more CPU.
The bottleneck may be:
- SQL queries
- EF Core N+1 queries
- Blocking code
- Thread pool starvation
- External APIs
- Large JSON responses
- Poor caching
- Connection management
- Middleware and logging
- Application logic
The most effective starting point is simple:
Measure where the request spends its time before changing the implementation.
Once you know whether the latency is coming from the database, application code, external dependencies, serialization, or infrastructure, the optimization path becomes much clearer.
For production ASP.NET Core applications, especially APIs supporting SaaS and enterprise workloads, monitoring P95/P99 latency and tracing dependencies can help catch performance regressions before they become user-facing problems.
Summary
CPU and memory use are not necessarily the only indicators of ASP.NET Core API performance issues. Latency can be caused by a variety of factors, including slow database queries, N+1 queries, blocking activities, external dependencies, big answers, cache choices, connection management, middleware, logging, and application logic. A useful method of troubleshooting performance without depending on conjecture is to measure the request, trace its dependencies, identify the real bottleneck, and measure again after each modification.
HostForLIFE.eu ASP.NET Core 10.0 Hosting
European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.
