The .NET garbage collector handles memory automatically, but frequent small allocations create GC pressure — the GC must periodically scan the heap, identify unreachable objects, and compact memory. In high-throughput ASP.NET Core applications, even small allocations inside frequently-called code paths accumulate into significant GC pauses. ArrayPool<T> provides a pool of reusable arrays, and stackalloc allocates fixed-size buffers on the stack, both avoiding heap allocation entirely for temporary buffers.
ArrayPool — Reusable Buffer Rental
// ── Rent and return pattern — the fundamental contract ─────────────────────
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096); // rent a buffer of at least 4096 bytes
try
{
// Work with the buffer — note: Rent may return a LARGER buffer than requested
int bytesRead = await stream.ReadAsync(buffer.AsMemory(0, 4096), ct);
ProcessData(buffer.AsSpan(0, bytesRead));
}
finally
{
ArrayPool<byte>.Shared.Return(buffer); // ALWAYS return — even on exception
}
// ── Using statement pattern — cleaner with IDisposable wrapper ─────────────
// System.Buffers.MemoryOwner<T> (Microsoft.Toolkit.HighPerformance)
// or write your own:
public sealed class PooledBuffer<T> : IDisposable
{
private readonly T[] _array;
private bool _disposed;
public PooledBuffer(int size)
{
_array = ArrayPool<T>.Shared.Rent(size);
Memory = _array.AsMemory(0, size);
}
public Memory<T> Memory { get; }
public Span<T> Span => Memory.Span;
public void Dispose()
{
if (!_disposed)
{
ArrayPool<T>.Shared.Return(_array);
_disposed = true;
}
}
}
// Usage with using — return is guaranteed
using var buffer2 = new PooledBuffer<byte>(4096);
int read = await stream.ReadAsync(buffer2.Memory, ct);
ProcessData(buffer2.Span[..read]);
ArrayPool<T>.Shared.Rent(minimumLength) returns an array of at least the requested length — it may return a larger array from the pool. Always track and use only the portion you need: buffer.AsSpan(0, actualSize), not buffer.AsSpan(). Returned arrays may contain data from previous users — never rely on the buffer being zeroed unless you explicitly zero it with Array.Clear() or buffer.AsSpan().Clear(). Clearing adds overhead, so only do it when the data is security-sensitive.ArrayPool for temporary buffers in hot paths — methods called per-request, per-message, or in tight loops. Benchmark before applying: allocating a small array on the heap costs microseconds; if the method is called once per HTTP request, the overhead is negligible compared to the database query. Profile with tools like BenchmarkDotNet and dotMemory to confirm allocations are a real bottleneck before optimising. Premature optimisation with ArrayPool makes code more complex for no measured benefit.try/finally or IDisposable wrapper to guarantee return. The pool does not have a finaliser that returns arrays — it is a cooperative, caller-managed system.Stackalloc — Stack-Allocated Buffers
// ── stackalloc — allocate on the stack, automatically freed on method exit ──
// Only valid for unmanaged types (int, byte, char, struct with no references)
// Size must be known at compile time or be a small constant
// Small fixed buffer — no heap allocation
Span<byte> buffer3 = stackalloc byte[256];
buffer3.Clear(); // zero it if security-sensitive
// Use it like any span
int length = FillBuffer(buffer3);
Console.WriteLine($"Filled {length} bytes");
// Conditional stackalloc — stack for small, heap for large
const int StackThreshold = 512;
int bufferSize = GetRequiredSize();
Span<char> chars = bufferSize <= StackThreshold
? stackalloc char[StackThreshold]
: new char[bufferSize]; // heap for large buffers
// Build a string without intermediate allocation
int written = FormatData(chars, data);
string result = chars[..written].ToString(); // one allocation at the end
When to Use Each Approach
| Approach | Use When | Limit |
|---|---|---|
new byte[n] |
Simple, infrequent, or large buffers | No limit (GC managed) |
stackalloc |
Very small, fixed-size, hot-path, sync only | ~1KB (stack overflow risk if too large) |
ArrayPool.Rent |
Large temporary buffers in hot paths | Any size; must return |
Memory<T> |
Async-compatible slice of pooled/rented buffer | Lifetime of underlying array |
Common Mistakes
Mistake 1 — Large stackalloc (stack overflow)
❌ Wrong — allocating too much on the stack causes StackOverflowException:
Span<byte> huge = stackalloc byte[1_000_000]; // will crash!
✅ Correct — use stackalloc only for small buffers (< 1KB); use ArrayPool for larger ones.
Mistake 2 — Using rented buffer data from a previous rental (dirty data)
❌ Wrong — rented array may contain data from previous usage:
byte[] buf = ArrayPool<byte>.Shared.Rent(256);
// buf may contain bytes from the previous user — read stale data!
✅ Correct — zero or overwrite the buffer before use if old data would cause incorrect behaviour.