Rust pipes need a drain order and an owner for every writer
Rust 1.87 added portable anonymous pipes. Reliable process supervision still needs concurrent draining, explicit writer ownership and bounded output handling.
A subprocess can stop making progress while every line around it looks reasonable. The parent waits for completion; the child waits for room to write its output. Rust’s standard anonymous pipes make that connection easier to build, but the application still owns the order of operations. Drain output while the producer can run, and make the lifetime of every writer explicit.
Looking back from November 2026, Rust 1.87.0 shipped on 15 May 2025 with anonymous pipes and integration with process input and output. The release’s example combines standard output and standard error, reads the resulting stream, then waits for the child. That order is part of the lesson, rather than incidental formatting around a new API.
Follow the dependency that can stop progress
Consider a proposed document-conversion service that launches an external converter and collects its diagnostics. This is an illustrative design, not a reported AL+ deployment. Small successful conversions might generate little output. A failed conversion could generate much more. If the parent waits before reading, the failure path can expose a circular dependency that ordinary successful inputs never reveal.
The child must finish writing before it exits. Its pipe has finite capacity. Once that capacity is occupied, a blocking write needs a reader to consume bytes. Meanwhile, the parent has chosen to wait for the child’s exit before doing that reading. Neither side can satisfy the condition on which the other now depends. Increasing a buffer merely moves the input size at which the design fails.
The version-pinned 1.87 pipe documentation describes blocking reads and writes, platform-dependent capacity and end of file after all writer copies close. Its header includes a compiler commit date; that is not evidence that this documentation page was published on that date. The release announcement supplies the dated availability claim. The pinned page supplies the mechanism.
That mechanism suggests a review question more useful than “does the process finish in the happy case?” Ask which operation must run to unblock each pending operation. Include the consumer of the collected output. A reader that forwards bytes into another full queue may recreate backpressure farther downstream, even though it has correctly avoided waiting on the child first.
End of file belongs to all writers
Draining before waiting handles only one dependency. A read-until-end operation needs all writers to close. A forgotten clone held by the parent can keep the stream open after the child exits. A descendant that inherits a writer can do the same. Process completion and stream completion therefore need separate explanations in the supervisor’s design.
Write down which component owns each writer, when it relinquishes ownership and whether a command builder or helper retains a copy. That account should cover error returns during spawning and cancellation after a timeout. Rust’s ownership system helps release owned handles, but it cannot infer that an otherwise valid long-lived owner has already outlived its protocol role.
Multiple output streams introduce another choice. Reading standard output completely before touching a separate standard-error pipe can leave the child blocked on standard error. Concurrent readers or a deliberately combined stream can avoid that particular dependency. Combining streams changes the information available to later consumers, so the choice belongs in the output contract.
A combined stream should not be advertised as an exact event timeline. Concurrent writes can interleave, and a byte stream does not supply application message boundaries. If the product needs structured diagnostics with reliable attribution, design framing and source labels explicitly. Joining two handles is a useful plumbing operation; it does not reconstruct the producer’s intent.
Draining is not a storage policy
Reading all output into memory is understandable in a small example. For a service accepting variable workloads, that choice creates a second resource question: how much diagnostic data may one conversion retain? Specify a bound or stream to a suitable destination. If output is truncated, make that visible to whoever diagnoses the failed operation.
Reaching a retention limit must not accidentally mean abandoning the reader while continuing to wait. A proposed policy might discard further bytes while still draining, or stop the process and perform deliberate cleanup. Which option fits depends on whether complete output is required for correctness. Neither is a universal recipe, and no such supervisor was built or tested for this article.
The business consequence is concrete. A stalled conversion can occupy a worker and delay unrelated documents. An unbounded capture can consume memory needed by other jobs. These are possible failure modes, not measured losses. A useful acceptance discussion names the largest supported output, cancellation behavior and what operators receive when a job cannot complete.
A higher-level process helper may be enough
The strongest counterargument is that many applications do not need custom pipes. An existing process helper can already coordinate capture and waiting for a straightforward command. Keeping that abstraction can reduce the number of ownership and cancellation decisions that application code must express. Standard-library availability alone is a weak reason to replace it.
Custom plumbing becomes useful when the application needs a particular topology, such as deliberate stream combination or forwarding between processes. Even then, a portable API does not guarantee a portable protocol. Review the exact output and lifetime assumptions on supported platforms, without depending on one machine’s observed capacity.
For custom software development, make process supervision reviewable through its dependencies: who drains, who closes each writer and who reaps the child. That record gives future maintainers a way to assess a change in topology. Rust 1.87 simplified creating the channel; dependable progress still depends on the application using it in an order that can finish.
Paul