> ## Content Index
> Fetch the complete content index at: https://devinside.cloud/llms.txt
> Use this file to discover other available public pages before exploring further.

# Behind ValueTask: A Custom IValueTaskSource and Its Reuse Protocol
- URL: https://devinside.cloud/behind-valuetask-a-custom-ivaluetasksource-and-its-reuse-protocol/
- Published: 2025-08-13T10:15:00.000Z
- Updated: 2026-11-12T17:08:24.000Z
- Description: An executable .NET source exposes the version, consumption and continuation rules behind reuse. The measurements show fewer allocations without a faster median in this fixture.
- Author: Marcin Borecki
- Tags: Engineering, Performance & Runtime, #rel07, #devinside-review

START WITH THE MECHANISM

Interactive companion and laboratory update: 12 November 2026\. The editorial date places the topic on the timeline; this current explanation and its measured evidence retain their actual preparation and execution dates.

THE FULL INVESTIGATION

## Behind ValueTask: A Custom IValueTaskSource and Its Reuse Protocol

Editorial context: 13 August 2025 · Prepared and updated: 12 November 2026

Dates and technology context

This is a retrospective editorial timeline. Topic context: 13 August 2025\. First publicly available: 12 November 2026\. Current text, visuals and laboratory revisions were prepared on 12 November 2026.

The actual wrapper, publishing guard, 32,767-use retirement policy, 18 checks and measured allocation/time comparison were created and executed in the 2026-11-12 revision.

*Prepared on 12 November 2026\. The accompanying laboratory targets net10.0 with SDK 10.0.400 and the locally verified runtime 10.0.15\. It is an educational single-operation provider, not a general-purpose production pool.*

Returning `ValueTask<int>` from a method is easy. Providing a reusable object behind that value is a substantially different job. The provider must distinguish operations that occupy the same object at different times, coordinate completion with continuation registration, preserve faults and cancellation, and know when the previous result has actually been consumed.

The tempting story is that replacing a task with reusable storage removes an allocation. Even when a measurement confirms that saving, the implementation has acquired a protocol. The object is no longer a permanently completed promise. It is storage whose meaning changes between operations. A late callback, a second consumer or an early reset can ask that storage the wrong question.

This article builds a small source around `ManualResetValueTaskSourceCore<int>`. It uses explicit state checks and a lock rather than presenting an ambitious lock-free abstraction. Its purpose is to make the contract inspectable and to demonstrate why reduced allocation is not automatically a better API.

The laboratory has no sockets or real I/O. It models the completion boundary of a provider so that the significant orderings can be controlled. It compares the complete wrapper with task-based alternatives, including the costs that remain around the reusable core. Correctness comes first; the benchmark is useful only after we know which behavior the implementations are supposed to preserve.

## A value returned to the caller is not the source itself

For a consumer, `ValueTask<T>` is an awaitable value with a restricted consumption contract. The documentation warns against multiple awaits and other combinations that consume the same value more than once. If a caller needs task-style repeated observation, the documented route is to convert once with `AsTask` and use that task. Conversion is a choice with its own cost, not permission to consume the original value again. [ValueTask documentation](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1?view=net-10.0&ref=devinside.cloud).

The implementation explored here uses a source-backed value. The caller receives a small wrapper that identifies an `IValueTaskSource<int>` and a token. The source interface supplies three operations: inspect status, register a continuation, and retrieve the result. The token accompanies those interactions so the provider can relate them to the intended operation. [IValueTaskSource documentation](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.sources.ivaluetasksource-1?view=net-10.0&ref=devinside.cloud).

That wrapper does not own an immutable copy of the result. In this design, it points back to the same reusable object. Copying the wrapper creates another way to refer to the same operation, not an independent right to consume it.

Imagine operation A completing with an integer, followed by operation B using the same object. A delayed reader holding A must not accidentally read B's value. Equally, the provider must not clear A's state merely because it has signalled completion. The consumer may still be waiting for its continuation to run.

These problems are consequences of reuse, not defects unique to asynchronous programming. Reusing any storage requires an agreement about when its old meaning has ended. With an awaitable source, that agreement spans the producer, the awaiter and the continuation scheduler.

## Give the educational provider a narrow contract

`lab/ReusableSource.cs` exposes `Begin`, `Succeed`, `Fail` and `Cancel`, plus the source-interface methods used by the awaiter. `Begin` returns the value and its token. The producer supplies the same token when completing the operation. This makes stale producer activity visible in the example as well as stale consumer activity.

The provider permits one outstanding operation and one consumer. It rejects overlapping begins, repeated completion, repeated result consumption and duplicate continuation registration. It does not implement a queue of operations, a socket API, a pool of source objects or automatic cancellation-token registration.

That last exclusion is important. `Cancel` is a controlled terminal transition invoked by the test producer. The example does not claim to solve the additional race between a real I/O callback and a cancellation registration. Such a provider would need an explicit winner policy and cleanup of the losing registration.

The source maintains four phases:

```csharp
private enum Phase { Idle, Pending, Completed, Consumed }

```

It also tracks whether the producer is currently publishing completion. That flag prevents a synchronous continuation from beginning a new operation while the previous call into the core is still returning. The need for this extra condition is easy to miss when completion and consumption appear adjacent in a small happy-path example.

All transitions use one private lock. This is intentionally conservative. The lock gives this small implementation a clear serialization boundary and allows a synchronous callback on the same thread to enter `GetResult` reentrantly. It does not make arbitrary callback code safe. A callback that blocks waiting for another operation needing the same lock can still create a deadlock. The laboratory's callbacks are bounded and nonblocking.

The result is a teaching component whose restrictions can be stated in a few paragraphs. Any proposal to deploy a more general version should begin by listing which restrictions change and which new tests and guarantees those changes require.

## Keep the mutable core in one place

`ManualResetValueTaskSourceCore<T>` provides core logic for a manually resettable source, including completion and continuation machinery. It is a mutable struct. The wrapper stores it as a field rather than repeatedly copying it through a property or passing independent copies between components. [Core API documentation](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.sources.manualresetvaluetasksourcecore-1?view=net-10.0&ref=devinside.cloud).

That storage choice is part of the implementation, not a stylistic detail. Two copies of mutable operation state can each look plausible while representing different realities. One copy can receive a result while another remains pending. A wrapper should make it obvious which core instance is authoritative.

Our wrapper is a sealed reference type. It owns one core and a private state machine. The public methods do not return the core, and the tests interact through the provider's contract. This keeps the example focused on reuse of one operation slot rather than aliasing of several mutable structs.

The core still needs a surrounding policy. Providing completion machinery does not decide when the application may reset it, whether a particular producer is authorized to complete the current operation, or whether this educational wrapper should tolerate repeated consumption. Those are decisions visible in our own code.

The distinction is similar to using a collection that is internally correct while still violating an application's ownership rule. A reliable building block does not infer the higher-level lifecycle the programmer intended.

## Reset only after consumption, and after publication unwinds

01 / See the idea

### Completion is not consumption

![Completion is not consumption. The educational wrapper separates four lifecycle conditions. Reset is legal only after the previous result was consumed and the publication call has returned.](https://devinside.cloud/content/images/2026/11/devinside/REL07-v1.svg)

[Enlarge diagram](https://devinside.cloud/content/images/2026/11/devinside/REL07-v1.svg)

The educational wrapper separates four lifecycle conditions. Reset is legal only after the previous result was consumed and the publication call has returned.

Interpretation and evidence
- Do not reset a completed but unread result.
- A throwing GetResult still consumes the outcome.
- Reentrant Begin is rejected during publication.
Runnable laboratory and deterministic correctness checks; not a production pool.

The most important method is `Begin`. It does not equate completed with reusable:

```csharp
if (publishing || phase is Phase.Pending or Phase.Completed)
    throw new InvalidOperationException("The previous operation is not retired.");
if (uses == 32767)
    throw new InvalidOperationException("This educational source is retired before token wrap.");
core.Reset();
uses++;
registered = false;
phase = Phase.Pending;
return (new ValueTask<int>(this, core.Version), core.Version);

```

This fragment runs under the provider's private lock. Reset prepares the core for the next operation. The exact runtime source shows the version being advanced and the previous continuation, error and result state being cleared. The implementation checked for this article is the tagged 10.0.15 source, not an assumption based on a later SDK reference page. [Tagged core implementation](https://raw.githubusercontent.com/dotnet/runtime/v10.0.15/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Sources/ManualResetValueTaskSourceCore.cs?ref=devinside.cloud).

In our protocol, the previous phase must be `Consumed`, except for the first use from `Idle`. Completion alone leaves a result available for its consumer. Resetting at that point would erase an obligation that has not ended.

Consumption alone is still insufficient during synchronous publication. A continuation can run as part of `SetResult`, call `GetResult`, and then attempt to start another operation before the outer producer call has unwound. This wrapper rejects that reentrant begin. After publication returns, beginning the next operation is allowed.

The restriction is deliberate and tested. It makes the retirement boundary stronger than simply observing that a result was read. A different implementation might support reentrant recycling with a carefully designed protocol, but this one does not pretend to do so.

This is a useful review question for any reusable asynchronous component: what exact event makes the storage reusable? If the answer alternates between signalled, completed, callback invoked and result consumed, the design probably needs a more precise state model.

## A short token is a guard, not an eternal identity

02 / Watch the mechanism

### The same object, a different operation

![The same object, a different operation. The token identifies a permitted use of one source object. The fixture retires before token wrap rather than presenting the short token as an eternal identifier.](https://devinside.cloud/content/images/2026/11/devinside/REL07-v2.svg)

The same object, a different operationREL07 / REUSABLE ASYNC STATEThe same object, a different operationOperation AToken 1 identifies this useConsume AIts obligation endsOperation BToken 2 rejects late work from AEducational protocol · no production-pool guarantee

Play animation[Enlarge diagram](https://devinside.cloud/content/images/2026/11/devinside/REL07-v2.svg)

Static view. Play to see how the mechanism unfolds.

The token identifies a permitted use of one source object. The fixture retires before token wrap rather than presenting the short token as an eternal identifier.

Interpretation and evidence
- Producer and consumer must use the right token.
- Copying ValueTask does not create another consumption right.
- The lifetime limit is a teaching-model restriction.
Runnable laboratory and deterministic correctness checks; not a production pool.

The token helps detect a reference to the wrong generation of the source. `Validate` checks it before status, registration, consumption and producer completion. The wrapper also rejects the consumed phase, because a matching token alone does not establish a remaining right to read.

```csharp
if (phase == Phase.Idle || token != core.Version)
    throw new InvalidOperationException("Wrong operation token.");
if (phase == Phase.Consumed)
    throw new InvalidOperationException("Operation already consumed.");

```

The token is a `short`, not an unbounded operation identifier. The laboratory retires the source after 32,767 begins, before cycling through the token space. This is intentionally wasteful compared with a specialized long-lived production provider. It lets the example make a simple statement: within this source's permitted lifetime, an old token will not become current again through wraparound.

The retirement limit is not a recommendation to put this threshold into every library. It is a boundary of the teaching model. A production component that reuses token values needs a broader argument about stale references, consumer discipline and lifetime. Increasing the size of an unrelated diagnostic operation ID would not change the token passed through `IValueTaskSource`.

The tests verify both sides of the generation boundary. A wrong token is rejected during the current operation. After consuming A and beginning B, A's producer token cannot publish a value into B, and A's consumer token cannot retrieve B's result.

This producer check is application-owned. The interface itself describes consumer interactions. Adding a producer handle in the laboratory makes another class of lifecycle mistake visible without claiming that every real I/O provider exposes the same API shape.

## Completion and continuation registration have two orders

03 / See the idea

### Two valid continuation orders

![Two valid continuation orders. A status check and continuation registration are not one atomic application step. The deterministic tests cover completion on either side of registration.](https://devinside.cloud/content/images/2026/11/devinside/REL07-v3.svg)

[Enlarge diagram](https://devinside.cloud/content/images/2026/11/devinside/REL07-v3.svg)

A status check and continuation registration are not one atomic application step. The deterministic tests cover completion on either side of registration.

Interpretation and evidence
- Test both orders deliberately.
- Do not use random load to create the only evidence.
- One registered consumer must receive one result.
Runnable laboratory and deterministic correctness checks; not a production pool.

An awaiter can discover a completed result and consume it without registering a continuation. Alternatively, it can see pending status and register work to resume later. There is also a significant boundary case: the operation can complete after the status check but before registration.

The laboratory tests those orderings explicitly. In the first, the producer succeeds before the value is awaited. In the second, an async consumer awaits the pending value before the producer succeeds. In the third, the test obtains pending status, completes the operation, and only then calls `OnCompleted`.

That third test is not random stress. It deliberately constructs the ordering that a concurrent environment could produce. The continuation must still be delivered once and must retrieve the result associated with the original token.

Our wrapper forwards the continuation, state, token and flags to the core after checking its own single-registration rule. `OnCompleted` is the interface boundary through which scheduling requirements are conveyed. [Core OnCompleted documentation](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.sources.manualresetvaluetasksourcecore-1.oncompleted?view=net-10.0&ref=devinside.cloud).

The wrapper does not build its own scheduler. It relies on the core for that machinery while preserving the application's lifecycle checks. Reimplementing continuation scheduling would add a second substantial problem to an article whose purpose is already narrower than a production provider.

The test callbacks capture exceptions into a completion signal where needed. This prevents a failure in an asynchronously invoked assertion from disappearing as an unexplained hang. The runner applies a bounded timeout to each case, but timing is not how the ordering is created.

## Synchronous continuations make reentrancy part of the API

The core exposes `RunContinuationsAsynchronously`. Enabling it forces asynchronous continuation dispatch; disabling it does not mean every possible registration must run inline. The configuration and ordering together determine the behavior that needs testing.

For the controlled no-context registration used in the inline case, the test verifies that the callback runs during the producer's completion call. Inside that callback, consuming the result is legal, while calling `Begin` is rejected because `publishing` remains true.

This distinction demonstrates why a completion method cannot always be read as an ordinary setter. It may execute consumer code before returning. The surrounding component must account for that code observing state, throwing, requesting further work or interacting with locks.

The queued test checks that the callback is not invoked inline on the producer's call stack. It does not require a permanently different thread identity. A thread-pool thread can later execute unrelated work, and thread identity alone is a poor definition of inline execution. The assertion combines thread identity with whether the producer is still inside the publication call.

Another test supplies the execution-context flow flag and uses an `AsyncLocal` value to verify that registration context reaches the callback. This is a focused check of the flag forwarded by this wrapper. It is not a complete test matrix of custom synchronization contexts or task schedulers, and the article does not claim support for every imaginable callback environment.

The lock-based teaching model has an explicit consequence: do not block a continuation while waiting for another thread that requires the same gate. The example is useful precisely because it makes this cost visible. A lower allocation count cannot justify hiding a reentrancy or deadlock risk from the API's users.

## A faulted result still has to be consumed

04 / A concrete example

### Consumption retires every terminal outcome

![Consumption retires every terminal outcome. All three terminal outcomes reach the wrapper's consumed phase through finally. Attempting GetResult while pending remains an error and does not retire the operation.](https://devinside.cloud/content/images/2026/11/devinside/REL07-v4.svg)

[Enlarge diagram](https://devinside.cloud/content/images/2026/11/devinside/REL07-v4.svg)

All three terminal outcomes reach the wrapper's consumed phase through finally. Attempting GetResult while pending remains an error and does not retire the operation.

Interpretation and evidence
- Preserve the original fault.
- Verify the cancellation token.
- Test a successful next operation after failure.
Runnable laboratory and deterministic correctness checks; not a production pool.

`GetResult` first rejects pending status. Attempting to retrieve a result early does not retire the operation. When the core is terminal, the wrapper retrieves the result inside a `try` and marks the operation consumed in `finally`:

```csharp
if (core.GetStatus(token) == ValueTaskSourceStatus.Pending)
    throw new InvalidOperationException("Result is not ready.");
try { return core.GetResult(token); }
finally { phase = Phase.Consumed; }

```

The `finally` matters because consumption can complete by throwing the operation's exception. If the wrapper advanced to consumed only after a successful integer return, a fault would strand the source in completed state forever. Cancellation has the same lifecycle issue: it is a terminal outcome to observe, not an excuse to skip retirement rules.

The tests verify that the original exception object is observed and that the cancellation token in the thrown cancellation exception matches the controlled token. They then begin and successfully consume another operation. This checks both propagation and cleanup, rather than merely asserting that something threw.

The wrapper represents controlled cancellation by publishing an `OperationCanceledException`. The tagged core's status logic distinguishes that exception from an ordinary fault. We do not add automatic cancellation registration, deadline timers or real I/O abort logic to this example.

Those exclusions avoid a misleading implication. A token being cancelled does not establish that an underlying operation stopped. A real provider must coordinate its terminal signal with the actual operation and release any associated resources. This laboratory tests the terminal protocol once the producer has chosen to publish cancellation.

## Incorrect consumption should be a visible failure in this fixture

The source deliberately rejects repeated result retrieval. This is stronger diagnostic behavior than consumers should assume from arbitrary `ValueTask` implementations. It teaches the single-consumption contract by making a mistake observable, rather than depending on an old value happening to remain in memory.

The duplicate-registration test installs one legitimate continuation, attempts to install a second, and then completes the operation. The second registration must fail without replacing the first. The legitimate consumer still receives the expected result.

Similarly, the duplicate-completion test ensures that publishing a second integer cannot overwrite the first terminal outcome. The stale-producer test ensures that an earlier operation cannot complete a newer one after reuse.

These are bounded, deterministic negative tests. The article does not encourage trying invalid operations against unrelated production providers to discover what happens. Different implementations may fail differently when their contracts are violated, and successful execution of an invalid pattern once does not make that pattern supported.

The fixture also does not claim to make arbitrary concurrency safe. One producer and one consumer remain the supported model. The gate protects their state transitions; it is not a license for a collection of unrelated consumers to race for the same result.

This is the value of a narrow teaching component. Its rejection paths explain the intended contract and make regressions testable without implying that the component has solved all the problems of a general reusable completion source.

## What the executed checks establish

The project uses a console assertion runner. From `lab/`, execute:

```text
dotnet run -c Release -- check

```

The completed local run reported 18 passing cases on runtime 10.0.15, with zero failures and zero skips. The source compiles under SDK 10.0.400, selected by the local `global.json`. The project disables runtime roll-forward and names the installed patch so the test and later measurement do not silently choose a newer runtime.

The cases cover completion before awaiting, registration before completion, and completion between status inspection and registration in both continuation configurations. They also cover fault and cancellation followed by reuse, pending and completed-but-unconsumed rejection, wrong and stale tokens, duplicate completion, duplicate consumption, duplicate registration, reentrant begin rejection, asynchronous dispatch, execution-context forwarding and retirement before version wrap.

This is evidence about the included component and those scenarios. It does not establish a production socket provider, an arbitrary synchronization-context matrix, a lock-free algorithm or fairness under contention. Those are absent from the implemented scope.

The tests avoid sleeps as a way to manufacture the key orderings. A timeout prevents a broken case from holding the run indefinitely, while explicit calls and completion signals determine when each event occurs. The wrap-limit test performs bounded sequential operations and verifies rejection at the documented boundary; it is a correctness test rather than a throughput experiment.

The source files remain part of the article's evidence. A future code change needs to preserve these tests or replace them with stronger checks derived from the same contract. A screenshot of a green run without the source version would make that relationship harder to audit.

## Compare complete usage patterns, not just a return type

The benchmark offers task and source strategies in three modes. Ready means the result is complete before the caller awaits it. Inline means the consumer registers while the operation is pending and completion may resume it synchronously. Queued uses asynchronous continuation dispatch.

For ready task results, the baseline uses `Task.FromResult` with values starting at 1,000\. The source alternative performs `Begin`, `Succeed` and one await. For pending task results, the baseline uses `TaskCompletionSource<int>`. Both pending strategies use an async helper returning `Task<int>` to expose the same consumer boundary.

That helper can allocate. Its presence is intentional and must remain in the interpretation. The benchmark is comparing these complete usage patterns, not promising that replacing a source object eliminates all surrounding async allocations.

The source itself and its lock are created once per measured batch. The task baseline does not reuse completed task objects for these changing results. Neither arrangement is a universal account of application behavior. A real API may have a high synchronous-completion rate, may cache some task results, or may need task-style repeated observation.

Stephen Toub's original explanation places `ValueTask` in that wider tradeoff: allocation savings are relevant in particular common patterns, while representation and consumption impose costs too. It is a reason to measure the actual pattern, not a blanket instruction to replace tasks. [Understanding ValueTask](https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/?ref=devinside.cloud).

Run a measurement only in the agreed exclusive lane:

```text
dotnet bin/Release/net10.0/SourceLab.dll measure task ready 10000
dotnet bin/Release/net10.0/SourceLab.dll measure source ready 10000

```

Replace ready with inline or queued for the other workloads. Each process performs 500 preparation operations using a separate instance, then measures a fresh batch. Counts are bounded below the educational source's retirement limit. The final checksum must equal an independently computed sum before a result is reported as correct.

## Read allocation and elapsed time as different evidence

The runner records process-wide managed allocation around the measured batch. This matters for queued continuations because work can move between threads. A caller-thread-only counter could miss part of the work, while a process-wide counter includes other managed activity in that process.

The reported elapsed time covers creation of the batch's source object, the loop, the selected completion pattern and result consumption. Counter reads and output formatting are outside the stopwatch interval. The fixed overhead of the harness still affects allocation totals, so tiny per-operation differences need proportionate interpretation.

Three fresh-process repetitions per configuration are intended to expose obvious variation, not establish a latency percentile or a broad statistical claim. Dividing batch time by operation count produces an average for that process. Taking a median across those averages is still not a p95 of individual requests.

No real I/O occurs. A queued completion is scheduler activity around an integer result, not a simulation of network latency, packet arrival or operating-system completion ports. If your application's workload is dominated by I/O, the relative importance of this local protocol may be very different.

The source's lock is also part of the measured design. If it makes a mode slower while reducing allocation, that is an informative tradeoff. Removing the lock merely to improve a chart would require a new synchronization argument and new tests, not just a faster loop.

A result where the source loses is just as relevant as one where it wins, because the article's purpose is to decide whether the extra protocol earns its place. The executed comparison below gives a concrete example of lower allocation without a corresponding improvement in median elapsed time.

## The measured source allocated less and did not run faster

05 / See the idea

### Fewer allocations, higher ready-path time

![Fewer allocations, higher ready-path time. The measured ready-path source allocated less but had a higher median elapsed time than the task baseline. Values are medians of three fresh-process averages, not per-request percentiles.](https://devinside.cloud/content/images/2026/11/devinside/REL07-v5.svg)

[Enlarge diagram](https://devinside.cloud/content/images/2026/11/devinside/REL07-v5.svg)

The measured ready-path source allocated less but had a higher median elapsed time than the task baseline. Values are medians of three fresh-process averages, not per-request percentiles.

Interpretation and evidence
- Bars use independent units and scales.
- The full article reports all three modes.
- A reusable core does not imply a faster operation.
benchmark-results.json; runtime10.0.15, 10000operations, ready mode, three samples per strategy

The integration owner ran all six strategy/mode combinations in separate processes, three times each, serially. Each measured batch contained 10,000 operations after the 500-operation preparation phase. All 18 samples reported runtime 10.0.15, the expected checksum and exit code zero. The raw observations are retained in `benchmark-results.json`.

The time column is the median of three process-level average times per operation. The range contains the minimum and maximum of those three averages. Allocation is likewise the median process-wide allocation delta divided by operation count. Neither the range nor the median is a distribution of individual request latency.

| Mode   | Strategy        | Median microseconds/op | Range of process averages | Managed bytes/op |
| ------ | --------------- | ---------------------- | ------------------------- | ---------------- |
| ready  | task            | 0.636                  | 0.632–0.646               | 72.0216          |
| ready  | reusable source | 0.958                  | 0.948–0.960               | 0.0216           |
| inline | task            | 1.171                  | 1.152–1.210               | 208.0216         |
| inline | reusable source | 1.229                  | 1.214–1.263               | 120.0216         |
| queued | task            | 1.117                  | 1.077–1.247               | 208.0584         |
| queued | reusable source | 1.171                  | 1.066–1.262               | 120.0584         |

The ready source path greatly reduced managed allocation in this fixture, while its measured median time was higher. The source's state checks, lock and lifecycle operations remain real work. Removing an allocated result object does not remove the protocol needed to make reusable storage meaningful.

The small nonzero allocation total in the ready source case is worth preserving rather than rounding to a promotional zero. The reported 0.0216 bytes per operation represents 216 bytes over the batch. This is an amortized batch observation, including fixed harness and source setup, not a claim that each individual invocation allocates a fractional object. Allocation-site tracing would be needed to assign each byte to a particular implementation detail.

The pending modes retained substantial allocation in both strategies. The Task-returning consumer helper remains part of the experiment, and the reusable source is not an end-to-end allocation-free async pipeline. Its observed reduction relative to the task baseline is useful information about this composition, not evidence that all applications using `ValueTask` will realize the same difference.

For inline mode, the source median was again higher. For queued mode, the process-average ranges overlapped. The source median was higher there too, but the limited repetition and uncontrolled background activity do not justify a precise general slowdown factor. The robust conclusion is narrower: this run does not provide an elapsed-time reason to adopt the educational source, despite its lower allocation counts.

The modes also should not be subtracted to estimate the intrinsic cost of scheduling. The queued task average happened to be lower than its inline average in this run, despite the additional dispatch behavior. Different process histories, execution paths and scheduling can influence short measurements. The experiment compares strategies within a declared pattern; it is not an isolated microbenchmark of every runtime mechanism involved.

No cold-start run, production I/O experiment or contention benchmark was performed. No measurement establishes the retained-memory cost of keeping many idle source objects in a pool. The test uses one source per batch, with a bounded sequential lifetime. If an actual system proposes pooling thousands of providers or handling overlapping operations, that proposal needs a different design and a separate experiment.

These limits do not make the measurements useless. They make the decision specific. For this fixture, ordinary task-based code is a serious baseline, and lower allocation alone has not shown that a custom source is worth its maintenance burden. A real application might still care about the allocation reduction, but it should demonstrate that need at its own boundary rather than inherit an optimization claim from this table.

## Know what would make the simpler task preferable

The task model is often the better fit when callers need to store the result, observe it repeatedly, compose it broadly or pass it to code whose consumption behavior is not tightly controlled. A custom source introduces a contract that every relevant path must respect.

Even in a performance-sensitive component, the first useful question is whether this allocation is important in the measured application. A source that reduces managed bytes in an isolated loop may add little value when a request performs substantial parsing, database work or network transfer. The operational cost of maintaining the source can exceed a small local saving.

Consider the maintenance questions exposed by this example. Who retires the object? How are producer callbacks associated with generations? Can a continuation begin another operation? What happens after a fault? Which cancellation event wins? What prevents token reuse from making an old reference appear current? What evidence verifies scheduler assumptions?

Those questions remain after the benchmark chart is drawn. The correct response is not necessarily to solve all of them with a more complicated implementation. It may be to retain `Task<T>`, narrow the component's scope, or use a well-tested provider already supplied by the platform.

The educational source is successful if it makes that decision clearer. It shows that reuse is a temporal ownership problem: the same object may serve another operation only after its previous obligations have ended. Reduced allocation is one possible consequence of getting that protocol right. It is not a substitute for the protocol.

## Source code and execution evidence

[Download the complete lab, source record and raw execution results](https://devinside.cloud/content/files/2026/11/REL07-lab.zip). This package supports the specific execution claims in this article. It is not a production certification.