Back to writing
published

Runtime-Neutral Infrastructure in Rust

How to build Rust libraries that stay useful across runtimes by owning the public API, isolating execution, and making blocking boundaries explicit.

Runtime neutrality is often described as “supporting async and sync.” That is too narrow. A runtime-neutral Rust library gives applications a stable semantic API without forcing them to adopt the library author’s executor, HTTP client, filesystem model, or task lifecycle.

This matters most for infrastructure: clients, protocol implementations, registries, and SDKs become dependencies of applications with very different operational constraints. A command-line tool, a web service, an embedded program, and a WASI-oriented application may all want the same domain behavior but not the same runtime.

Own the semantic boundary

Begin with types that describe the problem, not the implementation. A client may expose Request, Response, Headers, Body, and an error type that the library owns. Those types should be useful even when the transport changes.

Keep runtime-specific futures, sockets, channels, and generated protocol models behind private modules. If a transport must be supplied by the caller, define a narrow trait around the operations the library needs. This avoids making reqwest, Tokio, or a provider’s SDK the accidental public contract.

Runtime neutrality does not require hiding every dependency. It requires deciding which dependency owns which concern. An optional adapter can integrate with Tokio or another executor while the core remains usable without it.

Make blocking visible

Every I/O operation has a scheduling story. A synchronous function that performs network work should say so in its name and documentation. An async function should return a future and avoid secretly blocking the executor. If a compatibility layer bridges the two, the boundary should be explicit and the cost should be documented.

This is especially important for body streams. A body can be bytes, a reader, or an asynchronous stream, but those representations have different ownership and cancellation behavior. Choose a small internal model, provide deliberate conversions, and define what happens when a consumer stops reading. “It works in the demo” is not a backpressure policy.

Keep cancellation and limits in the contract

Timeouts, maximum body sizes, redirect limits, and connection reuse are not incidental knobs. Unbounded defaults turn a convenient library into an availability risk when it receives an unexpected response or a slow peer.

Cancellation should travel through the operation. Do not spawn an untracked background task simply to make an API easier to call. If work must outlive the caller, make ownership and shutdown explicit. This lets applications decide whether a request is best-effort, bounded by a deadline, or part of a longer-lived workflow.

Separate protocol from execution

Protocol code should answer questions such as “is this status successful?” and “how do these headers parse?” Execution code should answer “how do bytes move?” and “which runtime wakes the task?” Keeping those questions separate makes both easier to test.

For a transport adapter, test the semantic request-to-wire translation and the error mapping independently from the socket implementation. For the core, use deterministic inputs and small fake transports. The resulting tests are faster and tell you whether a failure is a protocol regression or an integration problem.

A practical architecture

A useful layout is a runtime-neutral core, private wire modules, and opt-in transport crates or feature flags. The core owns data types, validation, limits, and errors. A transport adapter owns connection behavior. A runtime integration owns task spawning and executor-specific conveniences.

The public API should not require callers to name all three layers. They should be able to choose the layer appropriate for their application and still receive the same semantic guarantees. Feature flags should remove optional integrations, not change the meaning of the core types.

The payoff is durable infrastructure. Applications can move runtimes, replace HTTP clients, or adopt a different deployment model without rewriting their domain code. The library remains honest about what it controls, explicit about what it limits, and flexible about how bytes are executed.