Repository navigation
wasi-http 0.3: request and response should have a tracing identifier #918
Description
In wasi-http 0.3, assumptions about instance reuse are changing: see #898. If other discussions exist where this has been discussed, please tag those in.
When instance reuse is considered the default, wasi-http embeddings can no longer depend on considering all output associated with a given instance to be rooted from a single incoming-request. Output includes prints to stdout and stderr, structured logs over other interfaces, outgoing requests (http and otherwise), database transactions, etc. No longer being able to observe what outputs are associated with what inbound request can be a significant user experience degradation (e.g. bytecodealliance/wasmtime#13404 (comment)) and could potentially block adoption of instance reuse if unsolved.
The essential problem now is that a p3 instance exporting a wasi-http handler has no way to assign an identifier to an owned request that corresponds to anything the callee considered the identity of that request. The outside world - whether the host calling into the handler, or another component doing the same - has a mechanism to uniquely identify each request it creates, and so does the guest (as a index into the guest instance's own table), but (by the component model's design) neither has a mechanism to map their own identifier to another's identifier.
The simplest possible thing we can do to make the same identifier legible to both sides of an interface is to give each request a method to retrieve an associated UUID. UUIDs are 128 bit numbers which commonly are presented as a string, but the representation as just 128 bits (morally a u128 which does not exist in the wit type system) is more compact and does not privilege any of the evolving mechanisms for string presentation.
So, I propose we add to the wasi http types wit:
record uuid { upper: u64, lower: u64 }
resource request {
/// Get the universally unique identifier associated with the request. For a given request, this always returns
/// the same value, and implementations must make best effort for that identifier to be universally unique.
get-id: func() -> uuid;
}The more difficult part of the design is that responses have the same problem. In the HTTP protocol a Response always corresponds with exactly one Request, but the wit interface (correctly, in my opinion) does not retain that property - the response resource can be constructed without any associated request. As a strawman, I'd suggest that we make a uuid an optional property of the response, to be provided in the constructor or set at most once, so that the getter can indicate that either no request is yet associated with this response, or that the same request is associated with the response for the entire lifetime of the response. If no uuid is ever set, it is not considered an error.
resource response {
/// added a constructor argument for optional uuid, which should only be set to the identifier from an
/// associated `request` if it is avaiblable.
new: static func(
headers: headers,
contents: option<stream<u8>>,
trailers: future<result<option<trailers>, error-code>>,
id: option<uuid>,
) -> tuple<response, future<result<_, error-code>>>;
/// Get the universally unique identifier associated with the response, if one has been assigned. If this returns
/// some, it will return that same uuid for the entire lifetime of the response.
get-id: func() -> option<uuid>;
/// Set the universally unique identifier associated with the response, if one has not yet been set. If one has been
/// set previously (including providing it in the constructor), returns an error. This identifier should only be set to the
/// identifier from an associated `request`.
set-id: func(id: uuid) -> result;
}Given its impact on the usability of the exciting new instance reuse features, I believe we should try to get this addition into wasip3 before it launches if possible.
Activity
lukewagner commentedon May 19, 2026
In P2, guest code can create and dispatch multiple requests and there can be multiple responses in flight as well, so if we want to preserve P2 logging behavior, I think that the thing to associate a logging call with is not quite a request or response.
Rather, what there was "only one of" before (when instances are not reused, of, if they are being reused sequentially, "at most one of at a time") and what we now have "one or more of at a time" in wasip3 is host calls to the root component's exported
handlefunction. I may be wrong (maybe @alexcrichton can comment on this), but I expect it's possible (and well-defined by the CM spec) for a natively-implemented (logging) import to ask Wasmtime "what's the host invocation that this async import call is associated with?". And then if the host assigns a "logging context id" with each host call tohandle(instead of each root component instance, as in P2), the host import can now associate log messages with the same logging context as it would have in P2 matching the P2 behavior. If all those assumptions hold, I think we wouldn't need any WIT-level changes in wasip3. But happy to discuss further!cfallin commentedon May 19, 2026
One related problem is the linkage between inbound requests and corresponding outbound requests from that handler. In that case at least it's not just a logging concern: some platforms may architecturally require that an outbound request be associated with some inbound request. In a world without concurrent async invocations to
handlewith multiple inbound requests, it's easy enough to associate outbound request(s) with the current inbound request. But in the new concurrent world, there is no resource plumbing that associates one with the other, and that's a big problem for some platforms (including, at least, the one within which I'm responsible for a wasi-http implementation).Aside from preserving that link for platform-architecture reasons, it seems like other concerns like accounting and limiting ("each inbound request can make K outbound requests") would need this durable link as well; and concurrent invocation of a reused instance would have a strictly worse determinism/developer-experience story otherwise.
The host-magic solution ("what's the host invocation that this async import call is associated with?") is fairly unsatisfying to me at least: it requires host-level tracking and introspection, therefore is not virtualizable (which is especially important as we consider middleware and handler composition for example), and also feels somewhat at odds with an otherwise capability-based design. (In essence, you are saying: there is an ambient capability to make outbound requests that is associated with the inbound request and dynamically-scoped to that request's async fn invocation.)
I wonder if it would make any sense to have the outbound request method (here) take a
parent-request: option<borrow<request>>parameter? I am not sure I like that design, either (for one, it will be quite Weird when rendered into the API of any reasonable HTTP client library in the guest environment), but absent a language/framework-level mechanism for "ambient dynamically-scoped capability", I can't think of another way to preserve the linkage above...lukewagner commentedon May 20, 2026
Yes, those are valid concerns and and alternative worth considering, so let's consider them.
So first, one practical concern with the
parent-requestapproach is that, from what I've seen, standard HTTP libraries (JS, Go, .NET, Java, Ruby/Rails, ...) have no such parameter in their API, and so it may often go unset which means that, if it's load-bearing, the features it enables may often break. Also, given the general problem statement in the OP, I don't think outgoing requests are the only place where we need this async context: any import call doing something "interesting" in the host (e.g., data store access) may need logging/quota/tracing/etc, and ifparent-requestis our only solution for this problem, we'll be in the game of manually adding aparent-requestparameter/field to every WASI and other imported API. Conversely, "incoming HTTP request" isn't the only concurrent external event triggering anasyncexport call: future WASI APIs (and certainly proprietary APIs) will define otherasyncexports taking other types of parameters and, for the same reasons as HTTP, they'll want concurrent logging/quota/tracing/etc to associate correctly.Looking at other language ecosystems, this problem isn't unique or specific to us here: most HTTP frameworks have their own version of this problem (b/c they all handle multiple HTTP requests running concurrently in the same "instance"), and the consistent theme for how they all solve this problem is, from what I can tell, to have some not-HTTP-specific {thread,task,async,context}-local-storage that is then used to store the current incoming HTTP request (and other contextually-relevant stuff, like OTel). And what I'm suggesting above is essentially doing likewise.
However, the "host magic" concern is a valid one, so let's dig into that too:
So in a multi-instance-but-single-
handle-call-per-instance world, the host can associate outgoing requests and logging calls with their originating incoming request via theStorestate and the reason we don't call this "host magic" is because you could hypothetically create a parent component (using donut- or hamburger-wrapping) containing multiple child component instances that tracks "which child instance is currently executing" in linear memory state by using the export-wrapper side to set the state (before calls tohandle) so that the import-wrapper side can read the state to know which child is calling. And even with multiple-sequential-handle-calls-per-instance the same technique would work.But of course with multiple-concurrent-
handle-calls-per-instance we lose this simple global-state technique b/c of the arbitrary interleaving ofhandleexecution. However, at any point in wasm execution, the CM defines a root-export-call (b/c every task (and coop thread) tracks its supertask all the way up to the root invocation from the host, and this definition essentially matches what the abovementioned HTTP frameworks would track using their {thread,task,async,context}-local-storage mechanism. While only the host can access this information atm, it could be readily exposed to the guest in a 0.3.0-backwards-compatible way and thus it's not necessarily host magic; it's just a concurrency feature we haven't added yet (b/c limited resources, like so many others). And such a feature would also be useful for many other similar kinds of use cases (that were possible via donut-wrapping in a sync-only context using global state but become hard in anasyncworld), and so this feature would move the virtualization power of components closer to the host, which is the broader goal. That being said, I don't know of anyone doing this kind of advanced wrapping atm, so, like many good-but-we-have-limited-resources ideas, I think we can wait until someone starts doing this kind of virtual-platform-layering for real, and then define/validate the feature in that context. (But also happy to discuss the potential shape of the feature sooner.)And again, the benefit (in addition to not needing to change the WASI interfaces and roll those changes out before Preview 3) would be that logging et al can Just Work without the guest SDK/libraries having to do anything much different than P2 and the solution doesn't just benefit HTTP requests/responses; other concurrent imports/exports would work similarly (e.g., OTel).
alexcrichton commentedon May 23, 2026
Currently this isn't implemented in Wasmtime, and I'm not sure that this would integrate well with the API. Right now we only have
TypedFunc::call_concurrentwhich is a Rust-asyncfunction for the result of the function, but there's nothing task-identifying there for example. We do have such information in the store but it's not exposed at the API layer at this time, and I'm not entirely sure it how it would be without contorting things a fair bit.Even if we could do this, however, wouldn't threads throw a wrench into identifying calls via the task tree? The origin of a thread I'd expect to most of the time not have much to do with the operation of the thread if it's long lived, for example.
alexcrichton commentedon May 28, 2026
I've attempted to further summarize, and document, the outcomes of all discussions in #920. I've currently scheduled that to close this upon merge, so please take a look!
Enable reflection on component async call stacks
Add async call stacks to the public API, use it in `wasi:http` (bytec…
Document concurrent instance reuse in `wasi:http/service`
Document concurrent instance reuse in `wasi:http/service` (#920)