Mastering ArrayPool<T> in .NET to Cut Down on Allocations


People might be familiar with object pooling in .NET with examples like HttpClient
and StringBuilder
. for this post we're going a level deeper, focusing on one of the most overlooked performance tools in .NET - ArrayPool<T>
. If your application performs frequent array allocations especially large ones used for file processing, network streams, or data transformations ArrayPool<T>
can significantly reduce pressure on the garbage collector and improve performance. ArrayPool<T>
is an API in .NET (part of System.Buffers
) that allows us to rent and return arrays instead of allocating new ones every time. Think of it as a recycling service for arrays, you take one when you need it and return it once you're finished. The idea is to avoid unnecessary allocations, particularly for large arrays, by reusing them efficiently from a shared pool. This approach is particularly beneficial when you're working in high throughput systems. Allocating the same size array over and over again in a loop, or in response to rapid fire requests, can quickly increase memory usage and lead to more frequent garbage collections. Instead, using ArrayPool<T>
ensures that you are borrowing memory that has already been allocated, saving both time and resources.
As an example, suppose you're reading from a stream. In a traditional implementation, you might allocate a new byte array each time you perform a read. This works fine for smaller workloads, but in a busy API or background service, these repeated allocations can become a performance bottleneck. By switching to ArrayPool<byte>.Shared
, you can rent a buffer from the pool, use it for the read operation, and then return it. This small change can give substantial improvements, especially in scenarios where performance and memory efficiency are critical.
When using ArrayPool<T>
, it's important to note that the array you receive may be larger than the size you requested. This behaviour is by design, as the pool is optimised for reuse rather than strict size matching. As a result, you should always check the actual length of the array before using it indiscriminately. Another consideration is that pooled arrays are not cleared between uses. If you are dealing with sensitive data or require clean arrays, it's best to manually clear them or request clearing during the return operation. Imagine a method that reads the contents of a file. Instead of calling File.ReadAllBytes
, which allocates a fresh array, you can rent a buffer from ArrayPool<byte>.Shared
, use it to read the data, and then copy the actual bytes into a new array sized exactly to the content length. You then return the rented buffer to the pool. This pattern allows you to handle large files without bloating memory usage, particularly when reading many files in succession.
The gains from using ArrayPool<T>
extend beyond basic file and stream operations. In more advanced scenarios such as working with System.IO.Pipelines
, gRPC, or high-performance custom serialisers the use of pooled arrays becomes essential. These technologies often rely on underlying buffer management strategies where pooling is already used under the hood, but having explicit control via ArrayPool<T>
lets you optimise even further when necessary.
While ArrayPool<T>
is incredibly useful, it should be used carefully. For example, it's ideal for larger buffers or frequently reused allocations, but probably not worth the added complexity for small, one off arrays. Also, care must be taken to ensure that buffers are not shared across threads or returned in an inconsistent state, as this can introduce bugs that are difficult to track down. ArrayPool<T>
is a powerful, low level performance tool that belongs in every .NET developer's toolkit. It helps you reduce unnecessary allocations, lower garbage collection overhead, and maintain high throughput in memory intensive operations. Whether you’re building APIs, processing data in background services, or optimising file I/O, array pooling is a simple but effective way to keep your application lean and responsive.
If you're interested in going even deeper, look at building your own pooled buffer strategies using IBufferWriter<T>
a common interface used in gRPC, SignalR, and custom serialisers for fine grained control over memory use.
Subscribe to my newsletter
Read articles from Patrick Kearns directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
