Your ASP.NET Core API is slow, so you add Task.Run() and expect things to get better.
It may look like a quick fix, but Task.Run() does not make every operation faster. In many cases, it only moves the same work to a thread-pool thread. For database queries, HTTP requests, and other I/O work, this does not solve the actual problem.
This is why it is important to understand how async and await work in your code. The goal is not to make every method async. It is to avoid blocking a thread while the application waits for an operation to finish.
What Do async and await Actually Do?
Many developers think async creates a new thread, but it does not create a new thread by itself.
When an ASP.NET Core endpoint waits for a database query, it does not need to keep a thread busy while waiting for the response.
public async Task<IActionResult> GetProducts()
{
var products = await _db.Products.ToListAsync();
return Ok(products);
}
The database may take some time to return the products, but the application does not need to keep a thread busy during that time. Once the database operation finishes, the method continues from await.
This allows an ASP.NET Core application to handle requests without keeping a thread busy during the wait.
The important thing is to understand what the code is waiting for, not just whether it uses await.
Why Task.Run() Is Often the Wrong Fix
Consider this code:
public async Task<IActionResult> GetProducts()
{
var products = await Task.Run(() =>
_db.Products.ToList());
return Ok(products);
}
Although the code uses await, the database call itself is still synchronous. Task.Run() only moves ToList() to a thread-pool thread. That thread stays busy while the synchronous database call runs.
For normal I/O work in ASP.NET Core, there is usually no reason to do this. Instead, use the async method provided by the library:
public async Task<IActionResult> GetProducts()
{
var products = await _db.Products.ToListAsync();
return Ok(products);
}
The better approach is to call the database's async method directly. This avoids wrapping the synchronous call in Task.Run(). Look at the operation behind await instead of adding Task.Run() just because a method looks slow.
Use Async for I/O Work
A web API often spends a lot of time waiting for other systems. It may query a database, call another API, read a file, or work with cloud storage. In these situations, an async API lets the application wait without keeping a thread busy.
Common I/O operations include:
- Database queries
- HTTP requests
- File operations
- Network calls
- Cloud storage
An HTTP request can be handled directly with the async API:
var response = await httpClient.GetAsync(url);
There is no need to put the HTTP call inside Task.Run().
The same applies to Entity Framework Core:
var user = await _db.Users
.FirstOrDefaultAsync(x => x.Id == id);
The application can wait for the database response without blocking a thread.
What About CPU-Heavy Work?
CPU-heavy work is different because the application is not waiting for another system. The CPU is actively doing the work.
For instance:
public byte[] GenerateReport()
{
// Expensive CPU calculation
}
In some situations, you may choose to move this work to another thread:
public async Task<byte[]> GenerateReportAsync()
{
return await Task.Run(() => GenerateReport());
}
However, Task.Run() does not make the calculation itself faster. The CPU still has to perform the same amount of work.
For CPU-heavy work, Task.Run() can be useful when you intentionally want to move that work to another thread. It should not be used as a general solution for database queries or HTTP requests.
Avoid .Result and .Wait() in Async Code
Another common issue appears when an async method is called but the result is then requested synchronously:
var result = GetDataAsync().Result;
The same problem can happen with:
GetDataAsync().Wait();
The method may be asynchronous, but .Result and .Wait() block while waiting for it to finish. Instead, let the async operation continue with await:
var result = await GetDataAsync();
This keeps the code easier to follow and avoids unnecessary blocking.
A simple rule is: if you have an async operation, use await instead of waiting for it synchronously with .Result or .Wait().
Do You Really Need async Here?
Another important point is that not every method needs to be asynchronous.
Consider this:
public async Task<int> Add(int a, int b)
{
return await Task.FromResult(a + b);
}
There is no real async work happening here. The method is only adding two numbers, so making it async adds unnecessary code.
A simple method is better:
public int Add(int a, int b)
{
return a + b;
}
Use async when the method has something meaningful to wait for, such as a database query, HTTP request, or file operation. For simple calculations that finish immediately, a normal synchronous method is usually clearer.
Keep the Async Flow Going
An ASP.NET Core application often has several layers:
Controller
↓
Service
↓
Repository
↓
Database
If the database operation is async, keep that async flow through the application layers rather than turning it into a synchronous call somewhere in the middle.
public async Task<User?> GetUserAsync(int id)
{
return await _db.Users
.FirstOrDefaultAsync(x => x.Id == id);
}
The service can await the result:
var user = await _userService.GetUserAsync(id);
The controller can then return the result:
return Ok(user);
Try not to call an async method and then use .Result or .Wait() in another layer. Keeping the async flow consistent makes the code easier to maintain and avoids unnecessary blocking in an ASP.NET Core application.
Find Out Why the API Is Slow
If your ASP.NET Core API is slow, adding Task.Run() should not be the first thing you try. First, find out where the time is actually going.
Check things such as:
- Database query time
- External API response time
- CPU usage
- Thread-pool usage
- Memory usage
- Garbage collection
- Number of requests
- Slow synchronous code
The SQL query may be slow, an external API may take two seconds to respond, or another part of the application may be using too much CPU.
Find the actual problem before changing the async code. Async code can handle waiting better, but it cannot make a slow database query run faster.
Our Take
At Qodors, we often see Task.Run() added when a method is slow or when a developer wants to make synchronous code async. A common case is a database call being wrapped in Task.Run() even though the database library already provides an async method.
Moving the synchronous call to another thread does not fix the database operation. It only changes where that work runs.
For normal ASP.NET Core API work, use the async methods provided by the library. With Entity Framework Core, that means methods such as ToListAsync() and FirstOrDefaultAsync(). For HTTP calls, use the async methods available in HttpClient.
Before adding Task.Run(), first ask:
Am I doing CPU-heavy work, or am I waiting for I/O?
If the application is waiting for I/O, use the proper async API. If the work is CPU-heavy, Task.Run() may be useful when there is a clear reason to move that work.
Quick Reference
- async does not create a new thread by itself.
- Use await for async operations.
- Do not wrap normal database calls in Task.Run().
- Use EF Core methods such as ToListAsync() and FirstOrDefaultAsync().
- Avoid .Result and .Wait() in async code.
- Do not make every small method async.
- Task.Run() does not make CPU work faster.
- Keep async calls going through your application layers.
- Find the real performance problem before changing your code.
- Use Task.Run() only when there is a clear reason for it.
Don't add Task.Run() just because an ASP.NET Core API feels slow. First find out what the application is waiting for, use async APIs for database and HTTP work, and avoid blocking calls such as .Result and .Wait(). When the work is CPU-heavy, use Task.Run() only when it actually fits the situation.
Top comments (0)