← Open in the full interactive course (progress tracking, search & more)

Same pooling idea as ArrayPool<T> — reshaped for a world where the memory isn't always a plain array.

The previous lesson solved a real problem: stop reallocating the same buffer over and over, rent it from a shared pool instead. But it solved it specifically for T[] — plain arrays. Recall two lessons back: Memory<T> exists precisely because not every scenario that needs a memory view can be pinned to a plain array — it might wrap native or unmanaged memory instead, memory an ordinary T[] can never represent. If your code is already working in terms of Memory<T> — because it's async, or because it's designed against an IMemoryOwner<T>-based API — ArrayPool<T>'s array-shaped contract doesn't quite fit.

In this lesson, you'll meet MemoryPool<T> — the Memory<T>-flavored counterpart to ArrayPool<T>, and you'll see how its IMemoryOwner<T> return value gives you automatic, RAII-style cleanup instead of a manual Return call.

What Is It?

The Simple Explanation

MemoryPool<T> is ArrayPool<T>'s sibling: same underlying idea — borrow reusable memory instead of allocating fresh each time — but it hands you back a Memory<T>-compatible object, wrapped in something that knows how to clean up after itself automatically.

The Technical Definition

MemoryPool<T> (in System.Buffers) is a resource pool for Memory<T>-based memory. MemoryPool<T>.Shared gives you the default, process-wide instance. Its core operation:

There's no separate Return method to call. Instead, IMemoryOwner<T> implements IDisposable — disposing it is what returns the underlying memory to the pool. That's the whole design difference from ArrayPool<T> in one sentence: ownership and cleanup are modeled explicitly, through the same IDisposable/using pattern you already use for files, connections, and every other disposable resource in .NET.

Why Does It Exist?

The Problem — Memory<T> doesn't have to be an array

Two lessons back, Memory<T> was introduced as the heap-friendly, storable counterpart to Span<T> — commonly backed by an array, but not required to be. It can also wrap native or unmanaged memory, memory obtained from platform interop, or other custom memory sources that were never a T[] to begin with. ArrayPool<T> is built specifically around lending out and reclaiming T[] instances — it has no vocabulary for pooling memory that isn't shaped like a plain array in the first place.

The Solution

MemoryPool<T> generalizes the pooling idea to whatever Memory<T> can represent, not just arrays. Its default (MemoryPool<T>.Shared) is commonly array-backed under the hood in the mainstream .NET implementation, but the abstraction itself — and any custom MemoryPool<T> implementation — isn't limited to that. Pairing it with IMemoryOwner<T> also solves a second, smaller problem: manual Return calls are easy to forget (as the previous lesson's Common Mistakes covered) — modeling ownership as a disposable object lets you lean on using to make returning automatic and much harder to skip.

Big Picture

ArrayPool<T>

MemoryPool<T>

How It Works

RENT → USE → AUTOMATIC RETURN VIA using
1. RENT — GET AN IMemoryOwner<T>
using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(8192);
2. USE IT VIA .Memory (AND .Span WHEN YOU NEED FAST ACCESS)
Memory<byte> buffer = owner.Memory;
int bytesRead = await upload.ReadAsync(buffer[..8192]); // Memory<T> survives the await, as covered previously

Span<byte> span = buffer.Span[..bytesRead]; // convert to Span<T> for the fast, synchronous part
ProcessChunk(span);
3. RETURN HAPPENS AUTOMATICALLY — NO EXPLICIT CALL NEEDED

Simple Example — the same upload loop, rewritten with MemoryPool<T>

async Task ProcessUploadAsync(Stream upload)
{
    using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(8192);
    Memory<byte> buffer = owner.Memory;

    while (true)
    {
        int bytesRead = await upload.ReadAsync(buffer[..8192]);
        if (bytesRead == 0) break;

        ProcessChunk(buffer.Span[..bytesRead]);
    }
    // owner.Dispose() runs automatically here, returning the memory to the pool —
    // no explicit Return call, no try/finally required
}

Compare this to the previous lesson's ArrayPool<T> version: the logic is identical — rent once, reuse across the loop, clean up when done — but there's no manual Return call and no explicit try/finally. The using declaration on owner handles both jobs at once, exactly the way it already handles closing a file or a database connection.

Real-World Example

MemoryPool<T> shows up specifically where code is already committed to the Memory<T>/IMemoryOwner<T> shape — often because it's crossing async boundaries or interoperating with lower-level, pipeline-style APIs:

For the far more common case — a plain, synchronous or simply-async buffer that's naturally a byte[]ArrayPool<T> remains the simpler, more directly-applicable tool, which is exactly why it's typically reached for first.

Analogy

A hotel key card vs. a library card you have to remember to return

Returning a rented ArrayPool<T> array is like a library card system where you personally have to remember to bring the book back — nothing stops you from just keeping it, and the library doesn't automatically know when you're done. MemoryPool<T>'s IMemoryOwner<T> is like a hotel key card tied to a checkout date: when your stay (the using scope) ends, access is revoked and the room is returned to the pool of available rooms automatically — you don't have to personally walk to the front desk and hand the card back for the system to work correctly.

Under the Hood

OWNERSHIP AS A FIRST-CLASS, DISPOSABLE CONCEPT
1. IMemoryOwner<T> MAKES "WHO'S RESPONSIBLE FOR RETURNING THIS" EXPLICIT
2. Dispose() IS WHERE THE ACTUAL RETURN HAPPENS
3. THE DEFAULT SHARED POOL IS OFTEN ARRAY-BACKED — BUT THAT'S AN IMPLEMENTATION DETAIL

Common Confusion

1. "MemoryPool<T> is strictly better than ArrayPool<T>, so I should always use it"

Not quite — they serve different shapes of code. If you genuinely need a plain T[] (many APIs still expect exactly that), ArrayPool<T> gives you one directly, with no extra indirection through Memory<T>/.Span. Reach for MemoryPool<T> specifically when your code is already working in Memory<T> terms — not as a blanket upgrade.

2. "Disposing the IMemoryOwner<T> clears the memory's contents, like ArrayPool<T>'s clearArray option"

Disposal returns the memory to the pool — it doesn't, by itself, guarantee the contents are zeroed for the next renter, mirroring ArrayPool<T>'s own default (non-clearing) behavior. If leftover contents matter for your scenario, that's a concern to handle deliberately, the same way it was in the previous lesson.

3. "I need MemoryPool<T> any time I'm working with Memory<T>"

Not at all — plenty of code creates a Memory<T> from an ordinary, non-pooled array (array.AsMemory()) with no pooling involved whatsoever. MemoryPool<T> is specifically for when you want the reuse/pooling benefit and you're working in Memory<T> terms — the two ideas (using Memory<T>, and pooling) are independent of each other.

Common Mistakes

Mistake 1 — Using owner.Memory after disposing the owner

Holding onto owner.Memory (or a Span<T> derived from it) in a variable that outlives the using block that owns it.

Memory<byte> leaked;
using (IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(1024))
{
    leaked = owner.Memory; //  still valid as a reference, but the pool now owns this memory again
} // owner disposed here — 'leaked' may be reused/overwritten by an unrelated future Rent

Never let a Memory<T> obtained from an IMemoryOwner<T> escape the scope that owns it — use it only while the owner itself is still alive and undisposed.

Mistake 2 — Reaching for MemoryPool<T> when a plain array would do

Adding MemoryPool<T> and its extra .Memory/.Span indirection to code that just needs a straightforward, synchronous byte[] buffer.

Default to ArrayPool<T> for plain array needs — it's simpler and gives you the array directly. Reach for MemoryPool<T> when the surrounding code is genuinely built around Memory<T>.

Mistake 3 — Forgetting the using declaration entirely

Calling MemoryPool<byte>.Shared.Rent(...) without assigning it to a using-declared variable — the RAII benefit only exists if you actually use the pattern.

Always pair Rent with using (either a using declaration or a using block) — that's the entire mechanism that makes automatic return work.

When Should I Use It?

Reach for MemoryPool<T> when

Reach for ArrayPool<T> when

Rule of thumb: Start with ArrayPool<T> by default — it's the simpler, more directly applicable tool for the common "I just need a reusable byte array" case. Reach for MemoryPool<T> specifically when you're already committed to Memory<T>/IMemoryOwner<T> elsewhere in the same code, and want its automatic, disposal-driven cleanup.

Mental Model

ArrayPool<T> = borrow a T[], remember to give it back yourself.
MemoryPool<T> = borrow an IMemoryOwner<T>, giving it back is Dispose()'s job.

Remember: same underlying idea — reuse instead of reallocate — different shape of API to fit different shapes of surrounding code. Pick based on whether your code already speaks T[] or Memory<T>.

Key Takeaway


Check Your Understanding

You've seen how MemoryPool<T> relates to, and differs from, ArrayPool<T>. Let's confirm the distinction is clear.

1. What does MemoryPool<byte>.Shared.Rent(4096) actually return?

Show answer

Correct: B

Why B is correct: MemoryPool<T>.Rent returns an IMemoryOwner<T> — an object that owns the rented memory, exposes it via .Memory, and returns it to the pool automatically when its Dispose() method runs.

Why A is incorrect: That's ArrayPool<T>.Rent's return type. MemoryPool<T> deliberately returns something different, precisely to support the ownership/disposal model this lesson covers.

Why C is incorrect: Rent doesn't return a Span<byte> directly — you get an owner, then access .Memory, and from there .Span when you need fast synchronous access.

Why D is incorrect: Rent is a synchronous method that returns immediately — no Task or await is involved in obtaining the buffer itself.

Reinforcement: The return type itself is the whole design story — IMemoryOwner<T> makes ownership and cleanup explicit, disposable concepts.

2. How does a rented MemoryPool<T> buffer actually get returned to the pool?

Show answer

Correct: B

Why B is correct: Disposing the IMemoryOwner<T> — whether called explicitly or triggered automatically by a using declaration/block — is precisely what returns the underlying memory to the pool. There's no separate, manually-invoked Return method.

Why A is incorrect: This describes ArrayPool<T>'s API shape, not MemoryPool<T>'s — MemoryPool<T> deliberately uses disposal instead of a separate return method.

Why C is incorrect: Nothing happens automatically just because the enclosing method returns — you need an actual using (or explicit Dispose() call) tied to the owner's scope for the return to occur.

Why D is incorrect: The memory absolutely can and should be returned — that's the entire point of the IMemoryOwner<T>/IDisposable pattern.

Reinforcement: using declarations pair naturally with MemoryPool<T> precisely because disposal is the return mechanism.

3. You're writing a synchronous helper method that just needs a temporary byte[] scratch buffer for a few lines of code, with no async involved and no existing Memory<T>-based API in the picture. Which is the more appropriate choice?

Show answer

Correct: B

Why B is correct: When a plain array is all that's genuinely needed, and there's no surrounding Memory<T>-based design to fit into, ArrayPool<T> is the simpler, more direct choice — you get the array immediately, with no extra layer of indirection.

Why A is incorrect: Neither pool is universally "more modern" or preferred — they're suited to different shapes of code, as this lesson's comparison covers directly.

Why C is incorrect: Pooling is entirely appropriate in synchronous, hot-path code — in fact, that's a very common and effective use case for ArrayPool<T> specifically.

Why D is incorrect: Combining both adds unnecessary complexity for no benefit here — pick the one tool that matches what your code actually needs.

Reinforcement: Choosing between the two pools comes down to what shape your surrounding code already is in — plain array code reaches for ArrayPool<T>, Memory<T>-based/async code reaches for MemoryPool<T>.

You now have both pooling tools in your kit, and know exactly when each one fits. Next: a precise, deep look at ref, in, and out — the parameter-passing modifiers that make some of this module's performance techniques possible in the first place.


dotnetmadeeasy.com — Learn C# and .NET, the right way.