An ASP.NET Core API can have normal CPU and memory usage and still feel slow.
That is one of the more frustrating API performance problems to troubleshoot.
You check the server. CPU looks fine. Memory looks fine. The database is available. There are no obvious infrastructure failures.
Yet an endpoint that should respond in 100–200 ms is taking 1–2 seconds.
The reason is that API latency is not determined by CPU and memory alone.
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 / blockingFor 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 msOptimizing 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 queriesThe 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 librariesFor 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 msYour 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:
IHttpClientFactoryAsync 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 candidateDepending on the application, you may use:
IMemoryCacheDistributed 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 processWhen 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 msOptimizing 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 rateStep 2: Break Down the Request
Measure:
API processing
Database
External APIs
Serialization
NetworkStep 3: Check Database Activity
Look for:
Slow queries
Missing indexes
Large result sets
N+1 queries
Repeated database callsStep 4: Check Asynchronous Code
Search the codebase for:
.Result
.Wait()
Blocking locks
Synchronous I/OStep 5: Trace External Dependencies
Find out whether the API is waiting on:
Payment services
Authentication providers
CRMs
Third-party APIs
Internal microservicesStep 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 msIt 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 msThose are the first areas worth investigating.
Now imagine the database profile reveals:
1 query
+
100 related queries
=
101 database callsFixing 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
ASP.NET Core API performance problems are not always visible through CPU or memory usage alone. Slow database queries, N+1 queries, blocking operations, external dependencies, large responses, caching decisions, connection management, middleware, logging, and application logic can all contribute to latency. Measuring the request, tracing its dependencies, identifying the actual bottleneck, and measuring again after each change provides a practical way to troubleshoot performance without relying on guesswork.

Join the conversation! Your thoughts help the community grow.