Skip to content

C# MemoryPool — Complete Guide

DodaTech Updated 2026-06-24 2 min read

In this tutorial, you'll learn about C# MemoryPool. We cover key concepts, practical examples, and best practices.

Your application allocates and discards byte buffers for network I/O, serialization, or file processing. Each allocation puts pressure on the GC, causing pauses. MemoryPool<T> provides a pool of reusable Memory<T> buffers, reducing allocations and GC pressure.

Wrong

public async Task ProcessStreamAsync(Stream stream)
{
    byte[] buffer = new byte[4096]; // New allocation every call
    int bytesRead = await stream.ReadAsync(buffer);
    // Buffer is garbage-collected when method exits
}

Output: Works. But byte[] is allocated per call, creating GC pressure in high-throughput scenarios.

public async Task ProcessStreamAsync(Stream stream)
{
    using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(4096);
    Memory<byte> buffer = owner.Memory;
    int bytesRead = await stream.ReadAsync(buffer);
    // Buffer is returned to the pool when disposed
}

Output: Same functionality. The buffer is rented from a shared pool and returned when disposed. No allocation after warmup.

MemoryPool<T>.Shared is a singleton that maintains a pool of arrays. The default pool uses ArrayPool<T> internally.

using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(4096);
Span<byte> span = owner.Memory.Span; // Access as span

Prevention

  • Use MemoryPool<T> for short-lived buffers in high-throughput paths.
  • Use IMemoryOwner<T> with using to ensure buffers are returned to the pool.
  • Use ArrayPool<T> when you need T[] directly instead of Memory<T>.
  • Use MemoryPool<T>.Shared for the default pooled buffer.
  • Rent the approximate size you need — the pool may return a larger buffer.
  • Return buffers promptly — holding them starves the pool.
  • Do not use pooled buffers as long-lived caches — they are meant for short-term use.

Common Mistakes with memory pool

  1. Placing the wildcard pattern first in case expressions, making all subsequent patterns unreachable
  2. Using head and tail instead of pattern matching, causing runtime errors on empty lists
  3. Forgetting that lazy evaluation defers computation until the value is forced, causing space leaks with unevaluated thunks

These mistakes appear frequently in real-world CSHARP code. DodaTech's contributors have identified these patterns through analysis of open-source projects and production systems.

Practice Exercise

Write a pure function that safely divides two integers using Maybe, then test it with edge cases like division by zero and negative numbers.

This exercise reinforces the concepts covered in this guide. Try implementing it before checking online solutions.

FAQ

What is the difference between MemoryPool and ArrayPool?

MemoryPool<T> returns IMemoryOwner<T> wrapping Memory<T>. ArrayPool<T> returns T[] directly. MemoryPool<T> is built on ArrayPool<T> internally. Use MemoryPool in async contexts where Memory<T> is needed; use ArrayPool for synchronous Span<T> work.

Can I rely on the buffer being exactly the size I requested?

No. The pool may return a buffer larger than requested. Check owner.Memory.Length for the actual size. The minimum rentable size is typically the pooled array bucket size (often powers of 2).

Does MemoryPool work with Span?

Yes. Access the span via owner.Memory.Span. Since Span<T> is stack-only, use this within synchronous code or before async operations.

MemoryPool<T> is used in Durga Antivirus Pro's scanning pipeline for zero-allocation buffer management. For more C# performance, visit DodaTech.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro