Allocating arrays is one of the most common sources of GC pressure in .NET applications. Every new byte[4096] eventually becomes garbage. In I/O-heavy workloads — reading files, processing network streams, handling HTTP requests — these allocations stack up fast. ArrayPool<T> exists to solve exactly this problem.

The Basics of ArrayPool<T>

ArrayPool<T> lives in System.Buffers and provides a thread-safe pool of reusable arrays. You rent a buffer, use it, then return it.

Example.cs
using System.Buffers;

var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(4096);

try
{
    int bytesRead = await stream.ReadAsync(buffer.AsMemory(0, 4096));
    ProcessData(buffer.AsSpan(0, bytesRead));
}
finally
{
    pool.Return(buffer);
}

There are two important details here. First, Rent may return an array larger than requested. If you ask for 4096 bytes, you might get 4096 or 8192 — the pool rounds up to its internal bucket sizes. Always track the actual length you need separately. Second, Return does not zero the array by default. If you are handling sensitive data, pass clearArray: true:

Example.cs
pool.Return(buffer, clearArray: true);

Shared vs Custom Pools

ArrayPool<T>.Shared is a singleton pool suitable for most scenarios. It has a maximum array size of 1,048,576 elements (2^20) and keeps up to 50 arrays per bucket.

For specialised needs, create a custom pool:

Example.cs
var customPool = ArrayPool<byte>.Create(
    maxArrayLength: 1024 * 1024 * 16, // 16 MB max
    maxArraysPerBucket: 10
);

Custom pools are useful when you need larger buffers or want to control how many arrays are cached. They are also useful for isolating pool usage between subsystems so one component cannot starve another.

MemoryPool<T>: A Higher-Level Abstraction

MemoryPool<T> wraps ArrayPool<T> and returns IMemoryOwner<T> instances that implement IDisposable, making lifetime management more explicit:

Example.cs
using System.Buffers;

using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(4096);
Memory<byte> memory = owner.Memory.Slice(0, 4096);

await stream.ReadAsync(memory);

The using declaration ensures the buffer is returned to the pool when the owner goes out of scope. This pattern is harder to misuse than raw ArrayPool<T> because forgetting to return becomes a missing Dispose call, which analysers can catch.

Real-World Example: File Hashing

Here is a practical example that hashes a file using pooled buffers:

Example.cs
public static async Task<byte[]> HashFileAsync(string path)
{
    using var sha256 = SHA256.Create();
    var pool = ArrayPool<byte>.Shared;
    byte[] buffer = pool.Rent(81920);

    try
    {
        await using var fs = File.OpenRead(path);
        int bytesRead;

        while ((bytesRead = await fs.ReadAsync(buffer.AsMemory())) > 0)
        {
            sha256.TransformBlock(buffer, 0, bytesRead, null, 0);
        }

        sha256.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
        return sha256.Hash!;
    }
    finally
    {
        pool.Return(buffer);
    }
}

Without pooling, this would allocate an 80 KB buffer on the Large Object Heap (any array over ~85,000 bytes lands there). With pooling, the buffer is reused across calls.

Common Mistakes

Forgetting to return. Every Rent must have a corresponding Return. Wrap the usage in try/finally or use MemoryPool<T> with using.

Using the full array length. Rented arrays may be larger than requested. If you pass the entire array to a stream read, you may read more data than expected. Always slice to the size you actually need.

Returning arrays you did not rent. Only return arrays that came from the pool. Returning a manually allocated array corrupts the pool's internal state.

Holding references after return. Once you call Return, the array may be handed to another consumer immediately. Any lingering references to the buffer are now pointing at shared mutable state.

When to Use ArrayPool

Use ArrayPool<T> when:

Skip it when arrays are small (under a few hundred bytes) and infrequent — the pool's overhead is not worth it for trivial allocations.

Summary

ArrayPool<T> is one of the most impactful, lowest-effort performance improvements available in .NET. It is used throughout Kestrel, System.IO.Pipelines, and ASP.NET Core internally. If your application processes data through buffers, pooling those buffers should be one of the first optimisations you reach for.