Skip to content

Bits of .NET

Daily micro-tips for C#, SQL, performance, and scalable backend engineering.

  • Asp.Net Core
  • C#
  • SQL
  • JavaScript
  • CSS
  • About
  • ErcanOPAK.com
  • No Access
  • Privacy Policy
C#

C#: Async/Await Common Mistakes That Kill Performance (And How to Fix Them)

- 01.02.26 | 01.02.26 - ErcanOPAK

Using async/await everywhere doesn’t automatically make your code faster – it can actually make it slower if done wrong. Here are the critical mistakes.

Mistake 1: Async All The Way Down (Unnecessarily):

// BAD: Unnecessary async overhead
public async Task GetUserIdAsync(string username)
{
    return await Task.FromResult(_cache.Get(username));  // Why async?
}

// This creates a Task, schedules it, waits for it, unwraps it
// Just to return a cached value that's already available!

// GOOD: Don't use async for synchronous work
public int GetUserId(string username)
{
    return _cache.Get(username);  // Direct return
}

// Or if you MUST return Task (interface requirement):
public Task GetUserIdAsync(string username)
{
    return Task.FromResult(_cache.Get(username));  // No async keyword
}

Performance Impact:
• Async method: ~50-100ns overhead per call
• Synchronous method: ~5ns
• In a hot path called 1M times: 100ms vs 5ms (20x difference)

Mistake 2: Blocking on Async Code (Deadlock Risk):

// BAD: Deadlock in ASP.NET
public ActionResult Index()
{
    var data = GetDataAsync().Result;  // DEADLOCK!
    return View(data);
}

public async Task GetDataAsync()
{
    var result = await _httpClient.GetAsync("https://api.example.com");
    return await result.Content.ReadAsAsync();
}

// Why deadlock:
// 1. GetDataAsync awaits GetAsync
// 2. Continuation tries to resume on ASP.NET SynchronizationContext
// 3. .Result blocks the thread, which IS the SynchronizationContext thread
// 4. Continuation waits for blocked thread
// 5. Blocked thread waits for continuation
// = DEADLOCK

FIX – Option 1: Async All The Way:

// GOOD: Never block, always await
public async Task IndexAsync()
{
    var data = await GetDataAsync();  // Properly await
    return View(data);
}

FIX – Option 2: ConfigureAwait(false):

// If you MUST block (library code, not ASP.NET):
public Data GetData()
{
    return GetDataAsync().GetAwaiter().GetResult();  // Better than .Result
}

public async Task GetDataAsync()
{
    var result = await _httpClient.GetAsync("https://api.example.com")
        .ConfigureAwait(false);  // Don't capture context
        
    return await result.Content.ReadAsAsync()
        .ConfigureAwait(false);
}

Mistake 3: Fire-and-Forget Without Error Handling:

// BAD: Silent failures
public void ProcessOrder(int orderId)
{
    _ = SendEmailAsync(orderId);  // Fires task, ignores exceptions!
}

public async Task SendEmailAsync(int orderId)
{
    await _emailService.SendAsync(...);
    // If this throws, exception is LOST FOREVER
}

// GOOD: Proper fire-and-forget
public void ProcessOrder(int orderId)
{
    _ = SendEmailWithHandlingAsync(orderId);
}

private async Task SendEmailWithHandlingAsync(int orderId)
{
    try
    {
        await _emailService.SendAsync(...);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to send email for order {OrderId}", orderId);
        // Maybe add to retry queue
        await _retryQueue.AddAsync(orderId);
    }
}

Mistake 4: Not Using Task.WhenAll for Parallel Operations:

// BAD: Sequential execution (slow)
public async Task<List> GetProducts(List productIds)
{
    var products = new List();
    
    foreach (var id in productIds)
    {
        var product = await _api.GetProductAsync(id);  // Waits for each
        products.Add(product);
    }
    
    return products;
}
// 10 products × 200ms each = 2000ms total

// GOOD: Parallel execution (fast)
public async Task<List> GetProducts(List productIds)
{
    var tasks = productIds
        .Select(id => _api.GetProductAsync(id))
        .ToList();
    
    var products = await Task.WhenAll(tasks);
    return products.ToList();
}
// 10 products in parallel = 200ms total (10x faster!)

Mistake 5: Using Task.Run for I/O Operations:

// BAD: Wastes thread pool threads
public async Task GetDataAsync()
{
    return await Task.Run(async () =>
    {
        return await _httpClient.GetAsync(url);  // Why Task.Run?
    });
}

// This:
// 1. Takes thread from pool to start Task.Run
// 2. That thread starts an async HTTP call
// 3. Thread returns to pool while waiting
// 4. Takes ANOTHER thread to process result
// = Wasted 2 threads for no reason

// GOOD: Just await I/O directly
public async Task GetDataAsync()
{
    return await _httpClient.GetAsync(url);  // No threads blocked
}

// Task.Run is ONLY for CPU-bound work:
public async Task CalculatePrimeNumbersAsync(int max)
{
    return await Task.Run(() =>
    {
        // CPU-intensive loop here
        return ComputePrimes(max);  // Synchronous CPU work
    });
}

Mistake 6: Returning Task Instead of ValueTask for High-Frequency Methods:

// BAD: Allocates Task on every call
public async Task TryGetFromCacheAsync(string key)
{
    if (_cache.TryGetValue(key, out var value))
    {
        return true;  // Still allocates Task
    }
    
    return await LoadFromDatabaseAsync(key);
}

// GOOD: Use ValueTask to avoid allocation when synchronous
public async ValueTask TryGetFromCacheAsync(string key)
{
    if (_cache.TryGetValue(key, out var value))
    {
        return true;  // No allocation!
    }
    
    return await LoadFromDatabaseAsync(key);
}

// Benchmark (1M calls, 90% cache hit rate):
// Task: 15MB allocated, 3 GC collections
// ValueTask: 1.5MB allocated, 0 GC collections

Decision Tree – When to Use What:

// Use async Task when:
// - Awaiting I/O operations (HTTP, DB, file)
// - Consumer needs to await the result
// - Not in a super hot path

// Use Task (no async keyword) when:
// - Returning already-completed tasks
// - Task.FromResult, Task.CompletedTask
// - Wrapping synchronous methods for interface compliance

// Use ValueTask when:
// - High-frequency method (called 1000s of times/sec)
// - Often returns synchronously (cache hits, validation)
// - Every allocation matters

// DON'T use async when:
// - Method is purely synchronous
// - Just wrapping Task.FromResult
// - Would cause unnecessary state machine generation

Related posts:

Use IAsyncEnumerable for Streaming Data in .NET

How to use SQL RAISEERROR() messages in C#

.NET Core — Response Compression Isn’t Enabled by Default

Post Views: 7

Post navigation

C# Performance: Use Span and Memory to Eliminate Array Allocations
C# Records: Immutable Data Classes with Built-In Value Equality (C# 9+)

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

April 2026
M T W T F S S
 12345
6789101112
13141516171819
20212223242526
27282930  
« Mar    

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (950)
  • How to add default value for Entity Framework migrations for DateTime and Bool (858)
  • Get the First and Last Word from a String or Sentence in SQL (836)
  • How to select distinct rows in a datatable in C# (805)
  • How to make theater mode the default for Youtube (753)
  • Add Constraint to SQL Table to ensure email contains @ (578)
  • How to enable, disable and check if Service Broker is enabled on a database in SQL Server (564)
  • Average of all values in a column that are not zero in SQL (531)
  • How to use Map Mode for Vertical Scroll Mode in Visual Studio (489)
  • Find numbers with more than two decimal places in SQL (447)

Recent Posts

  • C#: Use Init-Only Setters for Immutable Objects After Construction
  • C#: Use Expression-Bodied Members for Concise Single-Line Methods
  • C#: Enable Nullable Reference Types to Eliminate Null Reference Exceptions
  • C#: Use Record Types for Immutable Data Objects
  • SQL: Use CTEs for Readable Complex Queries
  • SQL: Use Window Functions for Advanced Analytical Queries
  • .NET Core: Use Background Services for Long-Running Tasks
  • .NET Core: Use Minimal APIs for Lightweight HTTP Services
  • Git: Use Cherry-Pick to Apply Specific Commits Across Branches
  • Git: Use Interactive Rebase to Clean Up Commit History Before Merge

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (950)
  • How to add default value for Entity Framework migrations for DateTime and Bool (858)
  • Get the First and Last Word from a String or Sentence in SQL (836)
  • How to select distinct rows in a datatable in C# (805)
  • How to make theater mode the default for Youtube (753)

Recent Posts

  • C#: Use Init-Only Setters for Immutable Objects After Construction
  • C#: Use Expression-Bodied Members for Concise Single-Line Methods
  • C#: Enable Nullable Reference Types to Eliminate Null Reference Exceptions
  • C#: Use Record Types for Immutable Data Objects
  • SQL: Use CTEs for Readable Complex Queries

Social

  • ErcanOPAK.com
  • GoodReads
  • LetterBoxD
  • Linkedin
  • The Blog
  • Twitter
© 2026 Bits of .NET | Built with Xblog Plus free WordPress theme by wpthemespace.com