Memory & Garbage Collection · Lesson 28

Pool leases, clearing and exception paths

Return rented buffers exactly once, expose logical lengths and include retained pool capacity in the budget.

Explore the interactive Memory Lab

Change owners, step through generations, pinning and finalization, or compare memory under load. The model explains concepts; the SDK examples measure real runtime observations.

Open the Memory Lab ↗

A pool trades work for retained capacity

ArrayPool can reuse arrays instead of allocating fresh storage for every operation. That can reduce repeated allocation work, but it introduces a lease protocol and retains capacity for future use. The pool is not a collector that makes memory disappear. A high-water-mark workload can leave more retained buffers than the current request count suggests, and the benefits depend on sizes, concurrency, clearing cost and actual reuse.

A pool should be evaluated as part of an ownership design. Identify which operation rents, which consumers borrow, and exactly which event ends the lease. A returned array can remain a perfectly reachable managed object while being invalid for the previous caller to access because another renter may now own it. Managed memory safety does not prevent that logical use-after-return defect.

Logical length and physical capacity

Rent guarantees at least the requested number of elements, not an array of exactly that length. A consumer must receive the logical payload slice, not the whole array by default. Otherwise stale or uninitialized content outside the written range can leak into a response, and a serializer may send more bytes than intended. Initialize every element the operation will read and keep lengths explicit through the pipeline.

Return must use the same appropriate pool and occur at most once for a lease. Double return can allow multiple callers to believe they own the same array, creating data races and corrupted output. A missing return can reduce reuse and increase retained in-flight ownership. Returning a foreign array or returning while a nested async consumer is still active violates the protocol even if a simple single-threaded example appears to work.

Clearing has a purpose and a cost

Clearing can prevent previous contents from being exposed to future renters or remove managed references held in reference-containing arrays. For secrets, an explicit zeroing policy should target the sensitive used region while the caller still owns the buffer, using an appropriate API such as CryptographicOperations.ZeroMemory for byte secrets. A pool's clear-on-return behavior is not a universal substitute for reasoning about when and where secrets must be erased.

Clearing full rented capacity can be more expensive than clearing a small logical payload, especially when the pool returns a much larger bucket. Conversely, clearing only the used region can leave unrelated prior contents outside it, so the next consumer must still obey logical lengths. Choose the confidentiality and retention policy deliberately, measure its cost, and never read a returned buffer afterward merely to inspect whether the pool cleared it.

Trace the portable lease

The program rents at least eight bytes, clears the logical eight-byte region, writes 42 into the first element and creates a separate string report while the lease is active. The report records capacity-ok:True, first:42 and logical:8. A finally block returns the array with clearArray:true, so the return happens even if work inside try throws.

Only the independent string is printed after the lease ends. The program never reads the returned array to verify clearing and never asserts an exact rented capacity. The final line states that no further buffer access occurs. That discipline is part of the example's correctness. Printing buffer[0] after Return would be an invalid lesson even if the same value happened to remain in a local test.

Test every terminal path

A useful pool test records rental and return ownership through a small tracking pool or wrapper. Inject failure after rent, during processing and before creating the result. Confirm exactly one return on each accepted lease, no return when validation rejects before rent, and no outstanding consumers when the return occurs. Test cancellation while an async consumer is actually pending rather than only before entry.

The interactive workload budget keeps retained idle pool capacity separate from active payload storage. If your measured pool number already includes active rentals, do not add both quantities without adjusting the accounting. The model explicitly assumes nonoverlapping contributions. In a real process, shared arrays and pool buckets require careful attribution, and a type histogram alone may not identify which component currently owns a particular buffer.

Design a lease-bearing component

Keep the owner private and expose only the intended logical view. Consider a narrow disposable lease type when ownership passes through multiple layers, but make copying and disposal semantics clear. A struct containing a reference to a lease owner can be copied; that does not automatically produce independent ownership. Prefer a design whose terminal action is idempotent at the owner and whose public API makes use after disposal fail clearly where feasible.

For an interview, compare allocating a small fresh result with pooling a large reusable workspace. The pooled workspace can be beneficial while the returned business result should own independent storage. Discuss backpressure, peak rentals, clearing policy and cancellation. A strong answer states how every path ends ownership and explains that a lower allocation rate can coexist with a higher retained heap.

Practice and review

Modify the portable program to reject negative and excessively large logical lengths before renting. Add a controlled processing exception and capture an independent return counter without touching the returned array. Verify the successful result remains independent of later pool reuse. If you need to test contents, inspect them during the lease or through a pool implementation designed for that test.

Review an async method that returns a task while its finally immediately returns the rented buffer. Identify whether the task has actually finished consuming the memory. Rewrite the method to await the consumer within the lease, then return in finally. Finish by drawing the distinction between reachable storage, valid ownership, exposed logical bytes and retained pool capacity.

A working example

using System.Buffers;
var pool = ArrayPool<byte>.Shared;
var buffer = pool.Rent(8);
string report;
try
{
    buffer.AsSpan(0, 8).Clear();
    buffer[0] = 42;
    report = $"capacity-ok:{buffer.Length >= 8};first:{buffer[0]};logical:8";
}
finally { pool.Return(buffer, clearArray: true); }
Console.WriteLine(report);
Console.WriteLine("lease-ended:no-further-buffer-access");

Expected output

capacity-ok:True;first:42;logical:8
lease-ended:no-further-buffer-access
Try this
Trace ownership before inferring collection. Expected portable output: capacity-ok:True;first:42;logical:8 lease-ended:no-further-buffer-access

Further reading

Interview questions for this lesson