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.
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.
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:
Rent(int minBufferSize = -1) — returns an IMemoryOwner<T>, an object that owns a rented block of memory and exposes it through a .Memory property.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.
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.
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.
T[]Return(array) call requiredSpan<T>IMemoryOwner<T>, exposing .MemoryDispose() — automatic with usingMemory<T> and async-friendly APIsusing IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(8192);
owner is a disposable object that owns the rented memory for as long as it's aliveArrayPool<T>.Rent, the actual buffer may be sized somewhat differently than the exact number requested — check owner.Memory.Length rather than assumingMemory<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);
owner was declared with using, its Dispose() runs automatically at the end of the enclosing scope — that's what returns the rented memory to the poolReturn call to remember, and no try/finally to write by hand — using already guarantees cleanup runs on every exit path, exceptions includedasync 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.
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:
System.IO.Pipelines) that are built around Memory<T> end-to-end rather than raw arrays.using-driven guarantee that a rented buffer is returned, rather than relying on every caller to remember a manual Return.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.
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.
ArrayPool<T>, the array itself carries no information about whether it came from a pool or who's responsible for returning it — that responsibility lives entirely in your code's discipline (the try/finally pattern from the previous lesson).IMemoryOwner<T> makes that responsibility a real, typed object: whoever holds the IMemoryOwner<T> owns the memory and is responsible for disposing it. Passing around the owner (rather than the raw Memory<T>) makes that responsibility explicit and transferable in a way plain arrays never could.owner.Dispose() — whether explicitly or automatically via using — is what triggers the pool to reclaim the underlying memory, conceptually equivalent to ArrayPool<T>'s Return, just invoked through the standard .NET disposal pattern instead of a bespoke method call.owner.Memory (or a Span<T> derived from it) after disposal is a misuse — exactly like reading from a rented ArrayPool<T> array after calling Return on it — and should be avoided the same way: don't hold onto the memory past the owner's lifetime.MemoryPool<T>.Shared commonly hands out memory backed by ordinary arrays under the hood — so in practice, its everyday performance characteristics resemble ArrayPool<T> closely.Memory<T>/IMemoryOwner<T> in the first place.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.
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.
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.
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.
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>.
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.
Memory<T>-based or async-friendly APIs and needs pooled backing memory to matchusing-guaranteed cleanup rather than a manually-called ReturnT[] — still extremely common, and often simpler when you don't need Memory<T>'s extra flexibilityMemory<T> — no reason to add the extra layertry/finallyArrayPool<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.
T[], remember to give it back yourself.IMemoryOwner<T>, giving it back is Dispose()'s job.T[] or Memory<T>.
MemoryPool<T> exists because Memory<T> doesn't have to be array-backed, so pooling for it needed its own API, distinct from array-only ArrayPool<T>.MemoryPool<T>.Shared.Rent() returns an IMemoryOwner<T> — an IDisposable whose Dispose() is what returns the memory to the pool.using var owner = MemoryPool<T>.Shared.Rent(...); gives you automatic, guaranteed-on-every-exit-path cleanup — contrasted directly with ArrayPool<T>'s manual, must-remember Return call from the previous lesson.MemoryPool<T> when your code is genuinely working in Memory<T>/async terms; reach for ArrayPool<T> — still extremely common, and often simpler — when a plain T[] is all you actually need.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?
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?
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?
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.