Common ArrayPool Mistakes11/14/2026 | 5 minutes to read | Open in Markdown
In this post I review common mistakes I find (and sometimes make) when using ArrayPool<T> in .NET and C#. Examples target the .NET 10 runtime and the corresponding BCL.
ArrayPool<T> lets you rent arrays from a shared pool. It's typically used to obtain a temporary buffer for intermediate work before copying results to a final destination. When a method cannot update input data in place or requires a larger buffer for intermediate calculations, ArrayPool<T> is often beneficial: it avoids allocating a new array for every operation, reducing GC pressure and improving performance. The most common generic types used with ArrayPool are byte and char.
Typical usage involves three steps:
- Rent an array from the pool.
- Use the rented array for operations.
- Return the array to the pool.
An application can create a custom pool to control array counts and sizes in the pool, or it can use the Shared pool. For most scenarios the shared pool is sufficient.
In the rest of this blog post, I explore the common mistakes that developers make while using ArrayPool<T>.
The samples below can be copied into a Program.cs file and run as a top-level program. Add the System.Buffers namespace with a using directive.
using System.Buffers;
ArrayPool
Mistake 1
A typical problem I observe (and make) is demonstrated in the Problem1() method:
void Problem1() { var buffer = ArrayPool<byte>.Shared.Rent(10); Enumerable.Sequence<byte>(0, 9, 1).ToArray().CopyTo(buffer); ArrayPool<byte>.Shared.Return(buffer, true); var rent = ArrayPool<byte>.Shared.Rent(10); // 👈 Console.WriteLine(string.Join(' ', rent)); }
In the sample, a byte array is rented with a requested size of at least 10 bytes. The shared pool returns a 16-byte array in this case. The sequence [0,1,2,...,9] is copied to the array, which is then returned to the pool. Next, another array is rented and its contents are printed (all 16 bytes are zero):
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
The problem is that the second array is not returned to the pool. While this does not cause a memory leak, as arrays are eventually garbage collected, it defeats the purpose of using ArrayPool. If the pool exhausts its available arrays it must allocate new ones, which degrades performance.
Mistake 2
The next code snippet shows another common mistake:
void Problem2() { var buffer = ArrayPool<byte>.Shared.Rent(10); Enumerable.Sequence<byte>(0, 9, 1).ToArray().CopyTo(buffer); ArrayPool<byte>.Shared.Return(buffer); // 👈 var rent = ArrayPool<byte>.Shared.Rent(10); Console.WriteLine(string.Join(' ', rent)); }
The Problem2() method performs similar steps: it rents an array, writes data to it, and returns the array. Then it rents a new array and prints its contents to the console:
0 1 2 3 4 5 6 7 8 9 0 0 0 0 0 0
This is not always a problem in code that fully controls all ArrayPool<T> usage. However, if an application or library writes sensitive data to arrays (for example, API keys or passwords), other code paths might observe that data when renting an uncleared array. When sensitive data is involved, clear the array on return:
ArrayPool<byte>.Shared.Return(rent, clearArray: true);
Mistake 3
void Problem3() { var buffer = ArrayPool<byte>.Shared.Rent(1); ArrayPool<byte>.Shared.Return(buffer); ArrayPool<byte>.Shared.Return(buffer); // 👈 var rent0 = ArrayPool<byte>.Shared.Rent(1); var rent1 = ArrayPool<byte>.Shared.Rent(1); Console.WriteLine(rent0 == rent1); // Writes 'True' on the console }
In the Problem3() method, the rented buffer is returned twice. As a result, the pool contains two references to the same array instance. This means that subsequent rentals (rent0 and rent1) may return the same object instance to different parts of the application simultaneously. Sharing the same array can lead to concurrency issues. Unfortunately, this error is difficult to detect or test.
MemoryPool
MemoryPool<T> is a lesser-known alternative to ArrayPool<T>. It provides similar functionality and API surface as the array pool, but the rented memory comes with the concept of ownership. The object returned from the Rent() method is a disposable owner object.
void MemPool() { using (var pooled0 = MemoryPool<byte>.Shared.Rent(10)) { Enumerable.Sequence<byte>(0, 9, 1).ToArray().CopyTo(pooled0.Memory); pooled0.Dispose(); pooled0.Dispose(); } using var pooled1 = MemoryPool<byte>.Shared.Rent(10); using var pooled2 = MemoryPool<byte>.Shared.Rent(10); Console.WriteLine(pooled1 == pooled2); // Writes 'False' on the console }
Rent() returns a memory owner object with a Memory property and a Dispose() method. After using a rented memory from a MemoryPool<T>, the developer disposes the object either by explicitly calling Dispose() or using a using statement. This returns the object to the pool. Hence the lifetime of the callsite of renting the memory, and callsite returning the same memory may become detached. A developer, does not need to manually track the owning pool of the array, it is implicitly handled.
The IMemoryOwner<T> implementation ArrayMemoryPoolBuffer ensures the object is returned to the pool only once, even if disposed multiple times.
Note that while MemoryPool<T> helps prevent some mistakes (like double returns and missing returns), it does not clear the returned array.
Conclusion
ArrayPool<T> and MemoryPool<T> provide allocation free access to temporary arrays, typically used for byte and char buffers. While the API is easy to use, developers must be aware of several caveats that are otherwise difficult to unit test. Good coding practices, code reviews can help avoid these mistakes and custom code analyzers can emit warnings for the above cases.
This post highlighted the most common errors and gave suggestions on how to address them.