If a method declared with the async modifier returns a cached result or completes synchronously,

rajesh yadav 231 Reputation points
2025-07-03T11:16:07.6466667+00:00

i could not understand so could i get an exmaple of cached result and syncronous completion of async method.

following is the lines from

https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios

"If a method declared with the async modifier returns a cached result or completes synchronously"

Developer technologies | ASP.NET | ASP.NET Core
0 comments No comments
{count} votes

Accepted answer
  1. Danny Nguyen (WICLOUD CORPORATION) 410 Reputation points Microsoft External Staff
    2025-07-04T08:33:04.25+00:00

    Hi there,

    Like Bruce has said, the document you provided refers to the performance issue occurs because Task is a reference type that gets allocated on the heap every time it's created. Since Task is a reference type, every time you create one, it allocates memory on the heap rather than the stack. This allocation takes time and uses memory that needs to be garbage collected later.

    The performance hit is particularly noticeable in two scenarios:

    1. Cached results - When an async method returns a value that's already cached/computed
         private static Dictionary<int, string> _cache = new();
          
         public async Task<string> GetUserNameAsync(int userId)
         {
             if (_cache.ContainsKey(userId))
             {
                 return _cache[userId]; // Cached data - no async work needed
                                       // But still creates a Task object on heap!
             }
             return await FetchFromDatabaseAsync(userId); // Real async work
         }
          
         // Problem: In a tight loop
         for (int i = 0; i < 10000; i++)
         {
             var name = await GetUserNameAsync(i % 100); // Creates 10,000 Task objects
                                                        // even though most are cache hits
         }
      
    2. Synchronous completion - When an async method actually completes synchronously (no real
         async work needed)
         public async Task<int> ValidateInputAsync(int value)
         {
             if (value < 0) return 0;       // Simple validation - completes immediately
             if (value > 100) return 100;   // Another sync path - completes immediately
                                            // Both still create Task objects unnecessarily!
             return await ComplexValidationServiceAsync(value); // Only this needs real async
         }
          
         // Problem: Most calls complete synchronously but still allocate Tasks
         for (int i = -50; i < 150; i++)
         {
             var result = await ValidateInputAsync(i); // Creates 200 Task objects
                                                      // even though most complete sync
         }
      

    If you're calling such methods repeatedly in a loop, you're creating many unnecessary Task objects, which can accumulate significant overhead.

    According to the documentation, the solution is ValueTask<T>, which can return values directly without heap allocation when no async work is needed, only creating a Task when actual async operations are required. ValueTask<TResult> Struct (System.Threading.Tasks) | Microsoft Learn

    public ValueTask<string> GetUserNameValueTaskAsync(int userId)
    {
        if (_cache.ContainsKey(userId))
        {
            return new ValueTask<string>(_cache[userId]); // No heap allocation!
        }
        return new ValueTask<string>(FetchFromDatabaseAsync(userId)); // Only allocate when needed
    }
    
    0 comments No comments

4 additional answers

Sort by: Most helpful
  1. AgaveJoe 30,126 Reputation points
    2025-07-03T12:00:54.7+00:00

    If a method declared with the async modifier returns a cached result or completes synchronously

    Cached results are typically held in memory. Fetching them asynchronously generally isn't beneficial and can even be resource-intensive, as it involves creating a new thread to retrieve data that's already readily available.

    As we discussed previously, wrapping asynchronous code in a synchronous method leads to wasted thread resources. The main thread will still wait for the asynchronous task to finish, negating the benefits of asynchronous execution.

    0 comments No comments

  2. TANMAY TIWARI 0 Reputation points
    2025-07-03T12:20:21.47+00:00

    You're right that accessing cached results is fast and generally doesn’t benefit from asynchronous execution. But I think it's important to clarify what actually happens when an async method returns a cached result or completes synchronously.

    When a method marked async returns a cached value using something like Task.FromResult(...), it does not spin up a new thread or create any unnecessary overhead. This is an efficient way to return an already-available result while still preserving the method's asynchronous signature. It's especially useful when the method may sometimes require async work (like fetching from a database), and sometimes return quickly (like from memory).

    For example:

    public Task<string> GetUserAsync(int id)
    {
        if (_cache.TryGetValue(id, out var user))
        {
            return Task.FromResult(user); // Efficient synchronous completion
        }
    
        return FetchUserFromDatabaseAsync(id); // Real async work
    }
    

    This pattern avoids blocking threads, avoids deadlocks, and ensures consistency for the caller — who always awaits the result.

    Regarding wrapping async code in a synchronous method: you're absolutely right that doing something like .Result or .Wait() on an async method can cause thread blocking or deadlocks, especially in ASP.NET environments. But that’s a separate concern from an async method returning synchronously.

    ✅ So to summarize:

    Returning a cached result via Task.FromResult(...) is safe and efficient.

    No new thread is created in this case.

    Problems typically arise only when sync code blocks on async, not when async completes synchronously.

    Hope this helps clarify the nuance!


    0 comments No comments

  3. Bruce (SqlWork.com) 78,236 Reputation points Volunteer Moderator
    2025-07-03T15:34:12.5466667+00:00

    the doc are referring to the fact that if the async action often return synchronously, due to caching, or other scenarios, then there is an overhead in returning Task<result>. this is because as a dummy task object has to be created. if the method is called a lot and returns sync a lot, then the allocation of the task is an unnecessary overhead. a better solution should be to return the ValueTask<result> which return an task or a value. this mean when the sync path is invoked, no task object is allocated:

    https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1?view=net-9.0

    note: you don;t need to use ValueTask, your return value could implement GetAwaiter() itself, than did not use allocations.


  4. SurferOnWww 4,721 Reputation points
    2025-07-04T01:42:22.1633333+00:00

    According to the Microsoft document Distributed caching in ASP.NET Core, the framework provides the following cache services:

    • Distributed Redis cache
    • Distributed Memory Cache
    • Distributed SQL Server cache
    • Distributed NCache cache
    • Distributed Azure CosmosDB cache

    Which one do you consider?

    If you use the Distributed Memory Cache, obtaining the cached data will not be I/O-bound task and the description "the extra allocations can accrue significant time costs in performance critical sections of code" in the document you refer to is true in ASP.NET.

    In addition, do you remember that I mentioned in your previous question "Do not use async/await for CPU-bound tasks. If you have CPU-bound tasks on ASP.NET, your best bet is to just execute it directly on the request thread"?

    If you use the Distributed SQL Server cache, the story will probably be different as it might include the I/O-bound task.

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.