IEnumerable vs. IAsyncEnumerable in .NET: Streaming vs. Buffering
You've likely used IEnumerable<T>
with EF Core while fetching data from a database. However, have you ever wondered how it loads data from your data set, and if IEnumerable
will work fine when the application grows? To find answers, I will break down the core mechanics of IEnumerable
and explore its async counterpart, IAsyncEnumerable
. I will also benchmark memory usage and execution time for each method with a real-life application, giving you practical insights for your next data-intensive application.

What is IEnumerable<T>
?
IEnumerable
is a synchronous interface that pulls data from the source and allows iteration using a foreach loop. It executes synchronously, meaning it can block the calling thread when dealing with slow data sources or large datasets. It was introduced in early versions of .NET and works by returning an enumerator via the GetEnumerator()
method, exposing MoveNext()
and Current
for iteration. Due to its compatibility with List<T>
, Array
, and Dictionary<TKey, TValue>
collections, many developers choose it as the default choice for data retrieval.
What is IAsyncEnumerable<T>
?
IAsyncEnumerable
is a generic interface that streams data asynchronously, one item at a time. It allows iteration using await foreach, enabling non-blocking execution by awaiting each item as it becomes available. .NET Core 3.0 and C# 8.0 introduced IAsyncEnumerable
in 2019 in System.Collections.Generic. It reduces memory overhead by processing each item as it becomes available, rather than loading the entire dataset upfront. In contrast to IEnumerable
, it fits in applications where the data source is prone to latency, such as database queries, file streams, and network responses.
Real-life example for IEnumerable<T>
and IAsyncEnumerable<T>
?
To study the impact of IEnumerable
and IAsyncEnumerable
on memory and execution time, we will test benchmarks using 100,000 and 1,000,000 rows. I am creating a console app with an in-memory database to keep things simple. Let's name the project IEnumIAsyncDemo
.
Step 1: Install the NuGet package
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.InMemory
Step 2: Define the model
namespace IEnumIAsyncDemo.Models;
public class Book
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
}
Step 3: Configure the DbContext
using IEnumIAsyncDemo.Models;
using Microsoft.EntityFrameworkCore;
namespace IEnumIAsyncDemo.Data;
public class BooksAppDbContext: DbContext
{
public DbSet<Book> Books => Set<Book>();
public BooksAppDbContext(DbContextOptions<BooksAppDbContext> options) : base(options) { }
}
Step 4: Create a seeding class
using IEnumIAsyncDemo.Models;
using Microsoft.EntityFrameworkCore;
namespace IEnumIAsyncDemo.Data;
public static class SeedData
{
public static async Task SeedAsync(BooksAppDbContext db)
{
if (await db.Books.AnyAsync()) return;
var books = Enumerable
.Range(1, 100_000)
.Select(i => new Book { Title = $"Book {i}", Author = $"Author {i}"});
await db.Books.AddRangeAsync(books);
await db.SaveChangesAsync();
}
}
Step 5: Prepare benchmark for memory and time evaluation
using System.Diagnostics;
namespace IEnumIAsyncDemo.Benchmarks;
public static class Benchmark
{
public static void Measure(string label, Action action)
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
var memoryBefore = GC.GetTotalMemory(true);
var stopwatch = Stopwatch.StartNew();
action();
stopwatch.Stop();
var endMemory = GC.GetTotalMemory(false);
Console.WriteLine($"{label} - Time: {stopwatch.ElapsedMilliseconds} ms, Memory: {(endMemory - memoryBefore ) / 1024} KB");
}
public static async Task MeasureAsync(string label, Func<Task> action)
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
var memoryBefore = GC.GetTotalMemory(true);
var stopwatch = Stopwatch.StartNew();
await action();
stopwatch.Stop();
var endMemory = GC.GetTotalMemory(false);
Console.WriteLine($"{label} - Time: {stopwatch.ElapsedMilliseconds} ms, Memory: {(endMemory - memoryBefore) / 1024} KB");
}
}
Step 6: Set Program.cs
using IEnumIAsyncDemo.Benchmarks;
using IEnumIAsyncDemo.Data;
using Microsoft.EntityFrameworkCore;
var options = new DbContextOptionsBuilder<BooksAppDbContext>()
.UseInMemoryDatabase("BooksDb")
.Options;
using var db = new BooksAppDbContext(options);
await SeedData.SeedAsync(db);
Benchmark.Measure("IEnumerable (Buffered)", () =>
{
var books = db.Books.ToList(); // buffers all
foreach (var book in books)
{
var temp = book.Title; // simulate light processing
}
});
await Benchmark.MeasureAsync("IAsyncEnumerable (Streamed)", async () =>
{
await foreach (var book in db.Books.AsAsyncEnumerable())
{
var temp = book.Title; // simulate light processing
}
});
I am setting up an in-memory database and seeding it with 100,000 records. The benchmark measures both IEnumerable
and IAsyncEnumerable
.
Step 7: Run the project and observe the results

Therefore, IAsyncEnumerable
is considerably faster and memory-efficient for a larger data set due to its streaming nature.
Step 8: Change the data sample size and observe the results
Let's change the line in the SeedData class to enlarge the dataset
var books = Enumerable
.Range(1, 1000000)
.Select(i => new Book { Title = $"Book {i}", Author = $"Author {i}" });
Now it will seed 1 million records. The results are:

It indicates that both take a similar time in execution, but IAsyncEnumerable
significantly impact memory usage.
Note: Since we're using an in-memory database, the async advantage is mostly visible in memory usage. In real-world scenarios (like network or database I/O), IAsyncEnumerable
typically performs even better due to non-blocking operations.
Conclusion
IEnumerable
and IAsyncEnumerable
are two different approaches to iterating over data in .NET. Developers predominantly use IEnumerable
without a second thought. However, it can burden memory when dealing with large amounts of data. In this case, IAsyncEnumerable
can assist as it does not buffer the data but streams asynchronously. In this post, I showed the difference between both using a prominent dataset example, and concluded that IAsyncEnumerable
is better in memory optimization for memory-sensitive applications.
elmah.io: Error logging and Uptime Monitoring for your web apps
This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.
See how we can help you monitor your website for crashes Monitor your website