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 / 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 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:

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:

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:

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:

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:

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:

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:

But caching introduces its own questions:

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:

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:

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:

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:

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.