Course contentsShow
← C#
Lesson 1746 of 1,90816. Performance and DiagnosticsFree lesson

Detecting Memory Leaks in Async State Machines

Students will identify how captured variables in async methods can inadvertently extend object lifetimes.

Detecting Memory Leaks in Async State Machines

In this lesson, you will learn how the compiler’s transformation of async methods into state machines can inadvertently trap objects in memory, causing silent, long-term memory leaks.

When you declare an async method, the C# compiler generates a hidden struct (or class, depending on the context) to manage the method's execution state. Crucially, any local variables that must persist across an await point are lifted into fields of this generated state machine.

The Captured Variable Trap

If your async method is an instance method, the state machine captures the this reference. More dangerously, if you are performing long-running operations—such as background tasks or event handlers—the state machine holds onto these captured variables until the method completes.

Consider this scenario:

public class DataProcessor
{
    private byte[] _largeBuffer = new byte[10 * 1024 * 1024]; // 10MB

    public async Task ProcessAsync()
    {
        var localData = _largeBuffer; 
        await Task.Delay(Timeout.Infinite); // The state machine keeps _largeBuffer alive
    }
}

In the example above, even if the DataProcessor instance is no longer needed by the rest of your application, the Task.Delay keeps the state machine alive. Because the state machine holds a reference to the DataProcessor instance (via this) and the localData reference, the 10MB buffer cannot be garbage collected. If this method is called repeatedly or kept alive indefinitely, you will see your memory footprint grow, even if you’ve "deleted" your references in the main program.

Why This Happens

The compiler must preserve the values of your local variables across await points so the method can resume correctly. These variables become roots for the Garbage Collector (GC) for as long as the Task returned by the method is incomplete. To prevent leaks, ensure that tasks are properly awaited or cancelled, and avoid capturing large objects in async methods that have long wait times. If you only need a portion of an object, extract exactly what you need into a local variable before the await and set the original reference to null if it is no longer required.

Key Takeaway: Because async state machines capture local variables and this references to persist state across await boundaries, they can unintentionally extend the lifetime of large objects, keeping them rooted in memory until the task completes.