30% OFF - Use code HANDSON30 for 30% off any course or Dometrain Pro! Browse courses →
  • Courses
  • Learning Paths
  • Blogs
  • Authors
  • Leaderboard
  • Dometrain Pro
  • Shopping Basket

    Your basket is empty

  • Business Portal
  • What is Asynchronous Programming in C#?

    October 4, 2025
    Listen to this blog 3m 56s
    Sign in to unlock the audio version of this article
    Sign in to listen

    Async code lets your app handle more work without blocking threads. In C#, async/await is the way to write non-blocking I/O that still reads nicely. This post covers basics, the core APIs you’ll use every day, and the mistakes to avoid, plus a few patterns that play nicely with ASP.NET Core.


    The mental model (quick)

    • Tasks aren’t threads. A Task is a promise of a result. For I/O, the OS wakes your task when data arrives, and no thread is parked waiting.
    • async/await is composition, not magic. await registers a continuation and returns control to the caller until the awaited work completes. Behind the scenes, a state machine is being generated to handle all of this for you.
    • I/O-bound vs CPU-bound. Async is for I/O (HTTP, DB, file, sockets). Use Task.Run only for short CPU work; you don’t want to block a request thread.

    Minimal API: pass the CancellationToken all the way down

    var builder = WebApplication.CreateBuilder(args);
    var app = builder.Build();
    
    app.MapGet("/weather/{city}", async Task<IResult> (string city, CancellationToken ct) =>
    {
    using var http = new HttpClient();
    http.Timeout = TimeSpan.FromSeconds(5);
    
        var json = await http.GetStringAsync($"https://example.com/weather/{city}", ct);
        return Results.Text(json, "application/json");
    });
    
    app.Run();
    
    • ASP.NET Core supplies a CancellationToken that fires when the client disconnects or a timeout happens.
    • Take it as a parameter and forward it into every async call. Free capacity when the request is no longer needed.

    Never block on async

    These patterns are trouble in the server code:

    // ❌ May cause thread pool starvation
    var result = SomeAsync().Result;
    
    // ❌ Queues extra work and blocks the request thread
    SomeAsync().Wait();
    

    Do this instead:

    // ✅ All the way async
    var result = await SomeAsync();
    

    Even though ASP.NET Core doesn’t have a classic UI synchronization context, blocking steals threads that the runtime could use for other requests.


    Compose multiple calls with Task.WhenAll

    using var http = new HttpClient();
    
    Task<string> u1 = http.GetStringAsync("https://api.test/users/1");
    Task<string> u2 = http.GetStringAsync("https://api.test/users/2");
    Task<string> u3 = http.GetStringAsync("https://api.test/users/3");
    
    var results = await Task.WhenAll(u1, u2, u3);
    
    • Fire requests first, then await them together.
    • Errors: WhenAll throws the first exception; you can inspect Task.Exception to see the aggregate if you need it.

    Throttle work: SemaphoreSlim or Parallel.ForEachAsync

    Hitting an API 1,000 times at once is impolite and often pointless. Cap concurrency.

    var urls = Enumerable.Range(1, 1000).Select(i => $"https://api.test/data/{i}");
    using var http = new HttpClient();
    using var gate = new SemaphoreSlim(10);
    
    var tasks = urls.Select(async url =>
    {
        await gate.WaitAsync();
        try
        {
            return await http.GetStringAsync(url);
        }
        finally
        {
            gate.Release();
        }
    });
    
    var data = await Task.WhenAll(tasks);
    

    Or use the built-in helper:

    await Parallel.ForEachAsync(urls, new ParallelOptions { MaxDegreeOfParallelism = 10 }, async (url, ct) =>
    {
        using var http = new HttpClient();
        await http.GetStringAsync(url, ct);
    });
    

    Timeouts without Thread.Sleep: WaitAsync

    Add a timeout to any task:

    var call = http.GetStringAsync("https://api.test/slow", ct);
    var withTimeout = call.WaitAsync(TimeSpan.FromSeconds(2), ct);
    var payload = await withTimeout; // throws TaskCanceledException after 2s
    

    Task.WaitAsync (available on .NET 6+) composes timeouts cleanly, no timers to manage.


    Cancellation patterns you’ll reuse

    Link tokens for per-call timeouts:

    using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
    cts.CancelAfter(TimeSpan.FromSeconds(3));
    var json = await http.GetStringAsync(url, cts.Token);
    

    Graceful background worker loop:

    public sealed class CleanupWorker(IServiceScopeFactory scopes, ILogger<CleanupWorker> log) : BackgroundService
    {
        protected override async Task ExecuteAsync(CancellationToken ct)
        {
            var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
            while (await timer.WaitForNextTickAsync(ct))
            {
                using var scope = scopes.CreateScope();
                var svc = scope.ServiceProvider.GetRequiredService<ICleanupService>();
                await svc.RunAsync(ct);
            }
        }
    }
    

    Async streams: IAsyncEnumerable<T>

    Stream results as they arrive to keep memory low and start sending data sooner.

    app.MapGet("/ticks", (CancellationToken ct) => GetTicks(ct));
    
    static async IAsyncEnumerable<string> GetTicks([EnumeratorCancellation] CancellationToken ct)
    {
        for (var i = 0; i < 5; i++)
        {
            ct.ThrowIfCancellationRequested();
            await Task.Delay(500, ct);
            yield return DateTime.UtcNow.ToString("O");
        }
    }
    

    Consume with await foreach:

    await foreach (var tick in GetTicks(ct))
    {
        Console.WriteLine(tick);
    }
    

    ValueTask the right way

    ValueTask can avoid an allocation when results are often already available. Don’t return it by default; use it for hot paths where you’ve measured a win.

    public interface ICache
    {
        ValueTask<string?> TryGetAsync(string key, CancellationToken ct);
    }
    
    public sealed class MemoryFirstCache(IMemoryCache cache, HttpClient http) : ICache
    {
        public ValueTask<string?> TryGetAsync(string key, CancellationToken ct)
        {
            if (cache.TryGetValue(key, out string? hit))
                return ValueTask.FromResult(hit); // no allocation
    
            return new ValueTask<string?>(FetchAndStoreAsync(key, ct));
        }
    
        private async Task<string?> FetchAndStoreAsync(string key, CancellationToken ct)
        {
            var value = await http.GetStringAsync($"https://api.test/data/{key}", ct);
            cache.Set(key, value, TimeSpan.FromMinutes(1));
            return value;
        }
    }
    

    Rules of thumb:

    • Don’t store a ValueTask for later.
    • Don’t await it multiple times.
    • If you don’t need its benefits, stick to Task.

    Library code and ConfigureAwait(false)

    In application code (ASP.NET Core), you generally don’t need ConfigureAwait(false). In libraries that may be used anywhere (including UI apps), add it to avoid capturing a synchronization context the caller might have:

    public async Task<string> LoadAsync(string path, CancellationToken ct)
    {
        using var fs = File.OpenRead(path);
        using var sr = new StreamReader(fs);
        return await sr.ReadToEndAsync(ct).ConfigureAwait(false);
    }
    

    Since .NET Core came out, we no longer have a synchronization context, so ConfigureAwait(false) became a lot less relevant.


    Common mistakes

    • Forgetting to pass CancellationToken. Accept it in endpoints and hand it down.
    • Blocking on async with .Result/.Wait(). Keep the chain async.
    • Overusing Task.Run. It doesn’t make I/O faster; it just burns a thread.
    • Ignoring backpressure. Throttle with SemaphoreSlim or Parallel.ForEachAsync.
    • Fire-and-forget without protection. If you must, capture exceptions and log them, or queue work to a background service.

    Handy snippets

    Timeout helper:

    public static class TaskExt
    {
        public static Task<T> WithTimeout<T>(this Task<T> task, TimeSpan timeout, CancellationToken ct = default) =>
            task.WaitAsync(timeout, ct);
    }
    

    Retry-and-cancel (without polly):

    public static async Task<T> RetryAsync<T>(Func<CancellationToken, Task<T>> action, int maxAttempts, TimeSpan delay, CancellationToken ct)
    {
        for (var attempt = 1; ; attempt++)
        {
            try 
            {
                return await action(ct);
            }
            catch when (attempt < maxAttempts)
            {
                await Task.Delay(delay, ct);
            }
        }
    }
    

    Wrap-up and next step

    Async isn’t about making everything run in parallel; it’s about not wasting threads while the OS does I/O. Keep the chain async, pass cancellation, compose with WhenAll, and throttle when needed. If you want a structured path with many more patterns (channels, pipelines, async LINQ, deadlock traps, fine-grained cancellation), check out From Zero to Hero: Asynchronous Programming in C# on Dometrain.

    About the Author

    Nick Chapsas

    Nick Chapsas

    Nick Chapsas is a .NET & C# content creator, educator and a Microsoft MVP for Developer Technologies with years of experience in Software Engineering and Engineering Management.

    He has worked for some of the biggest companies in the world, building systems that served millions of users and tens of thousands of requests per second.

    Nick creates free content on YouTube and is the host of the Keep Coding Podcast.

    View all courses by Nick Chapsas

    What's New

    Getting Started: Model Context Protocol (MCP)
    course

    Getting Started: Model Context Protocol (MCP)

    Learn how to get started with the Model Context Protocol (MCP) and integrate it into your applications.

    Learn more about Getting Started: Model Context Protocol (MCP)
    Hands-On: Learn TypeScript
    course

    Hands-On: Learn TypeScript

    Learn TypeScript through hands-on coding exercises. Practice what you learn with interactive challenges designed for every level.

    Learn more about Hands-On: Learn TypeScript
    Hands-On: Learn JavaScript
    course

    Hands-On: Learn JavaScript

    Learn JavaScript through hands-on coding exercises. Practice what you learn with interactive challenges designed for every level.

    Learn more about Hands-On: Learn JavaScript
    Hands-On: Data Structures & Algorithms in C#
    course

    Hands-On: Data Structures & Algorithms in C#

    Master data structures and algorithms through hands-on coding exercises in C#. Free to enroll for 7 days!

    Learn more about Hands-On: Data Structures & Algorithms in C#
    Blogsmith.ai
    feature

    Blogsmith.ai

    Turn your videos into blogs and newsletters with AI. Check out our new product at blogsmith.ai.

    Learn more about Blogsmith.ai
    Leaderboard
    feature

    Leaderboard

    See how you stack up against other learners. Track your progress, climb the ranks, and compete with the Dometrain community.

    Learn more about Leaderboard
    Hands-On: Learn PostgreSQL
    course

    Hands-On: Learn PostgreSQL

    Learn PostgreSQL through hands-on coding exercises. Practice what you learn with interactive challenges designed for every level.

    Learn more about Hands-On: Learn PostgreSQL
    Free Hands-On: C# for Beginners
    course

    Free Hands-On: C# for Beginners

    Learn C# through hands-on coding exercises. Practice what you learn with interactive challenges designed for everyone, from beginners to experts.

    Learn more about Free Hands-On: C# for Beginners
    Getting Started: AI for .NET Developers
    course

    Getting Started: AI for .NET Developers

    Get started with integrating AI into your .NET applications effectively using the latest LLM best practices.

    Learn more about Getting Started: AI for .NET Developers
    Getting Started: Building .NET Applications on AWS
    course

    Getting Started: Building .NET Applications on AWS

    Learn how to build and deploy .NET applications on AWS using CDK, Lambda, DynamoDB, S3, and more.

    Learn more about Getting Started: Building .NET Applications on AWS
    What's new in C# 14
    blog

    What's new in C# 14

    This guide covers every new C# 14 feature, explains its benefits, and provides practical code examples to help you navigate how you can use them.

    Learn more about What's new in C# 14
    Let's Build It: AI Chatbot with RAG in .NET Using Your Data
    course

    Let's Build It: AI Chatbot with RAG in .NET Using Your Data

    Build a Retrieval-Augmented Generation (RAG) chatbot that can answer questions using your data.

    Learn more about Let's Build It: AI Chatbot with RAG in .NET Using Your Data
    From Zero to Hero: SignalR in .NET
    course

    From Zero to Hero: SignalR in .NET

    Enable enterprise-grade real-time communication for your web apps with SignalR.

    Learn more about From Zero to Hero: SignalR in .NET
    Deep Dive: Solution Architecture
    course

    Deep Dive: Solution Architecture

    Master solution architecture and turn business needs into scalable, maintainable systems.

    Learn more about Deep Dive: Solution Architecture
    Migrating: ASP.NET Web APIs to ASP.NET Core
    course

    Migrating: ASP.NET Web APIs to ASP.NET Core

    A step-by-step process to migrate ASP.NET Web APIs from .NET Framework to ASP.NET Core.

    Learn more about Migrating: ASP.NET Web APIs to ASP.NET Core
    Getting Started: Caching in .NET
    course

    Getting Started: Caching in .NET

    Let's make the hardest thing in programming easy for .NET software engineers.

    Learn more about Getting Started: Caching in .NET
    From Zero to Hero: Testing with xUnit in C#
    course

    From Zero to Hero: Testing with xUnit in C#

    Learn how to test any codebase in .NET with the latest version of xUnit, the industry-standard testing library.

    Learn more about From Zero to Hero: Testing with xUnit in C#
    Create a ChatGPT Console AI Chatbot in C#
    blog

    Create a ChatGPT Console AI Chatbot in C#

    This walkthrough is your hands-on entry point to create a basic C# console application that talks to ChatGPT using the OpenAI API.

    Learn more about Create a ChatGPT Console AI Chatbot in C#