HTTP and gRPC Transport Channels
CritterWatch publishes telemetry to (and accepts control commands from) the Wolverine applications it monitors over a Wolverine transport endpoint. The default story — and the one Message Flow walks through — is the broker route: the monitored app and CritterWatch both attach to a common RabbitMQ / SQS / Azure Service Bus queue.
This page covers the broker-less alternative: pointing CritterWatch directly at the monitored app's HTTP or gRPC endpoint. The monitored app exposes a Wolverine.Http (/_wolverine) or Wolverine.Grpc transport mapping; CritterWatch publishes to that URL or host:port using ToHttpEndpoint / ToGrpcEndpoint. Request/reply-shaped traffic — the most common shape for CritterWatch's control + query commands — reads the reply straight off the HTTP/gRPC response slot, with no extra listening endpoint, no ReplyListener, and no message broker in the path.
This is the communication channel, not the node registry.
The transport channel is what carries CritterWatch's wire messages between the console and a monitored Wolverine service. It does not replace a clustered app's persistence-backed node registry or leader election — those still rely on the monitored app's Postgres / SQL Server / EF Core durability table. See Clustering for the durability side.
At a glance — what changes for operators
| Broker route (default) | HTTP / gRPC route | |
|---|---|---|
| What you stand up | RabbitMQ / SQS / Azure Service Bus queue | None — uses the service's existing HTTP or gRPC ingress |
| Where commands go | Queue, with broker fan-out | Direct point-to-point call to the service |
| Console-side listener | Required (reply queue) | None — replies ride the HTTP / gRPC response slot |
| Console outage | Telemetry buffers in the broker; replays on reconnect | Publisher's retry policy only; persistent outage = lost telemetry |
| Failure shape | Queue depth grows, then alerts | HTTP/gRPC status surfaces on the console call site immediately |
| Best for | Heavy telemetry, multi-consumer fan-out | Single-console-to-single-service control + query traffic |
The two routes are not mutually exclusive. A common shape is broker for telemetry (where store-and-forward matters) plus HTTP for commands (where round-trip latency matters); Wolverine routes each message type to its configured destination, so the split is config, not architecture.
When to reach for this
Choose HTTP or gRPC over a broker for:
- Services that already expose HTTP or gRPC and don't run a broker. No need to stand up RabbitMQ just to give CritterWatch a channel.
- Request/reply shaped traffic — control commands ("rebuild this projection"), query commands ("give me the source code for this handler chain"). The inline response-slot reply is faster than a broker round-trip and one fewer moving part in the failure analysis.
- A topology where a broker is operationally inconvenient — a small number of services, an environment that doesn't ship a broker by default (e.g. internal SaaS tooling running in containers without the broker sidecar), or a regulated environment that wants to keep the monitoring traffic on the same secured ingress as the application traffic.
Choose a broker over HTTP / gRPC for:
- Heavy telemetry fan-out — broadcasting a single
ServiceUpdatessnapshot to multiple consumers (e.g. a CritterWatch cluster + an independent metrics pipeline). The broker fans out for free; the inline channel is point-to-point. - Strict store-and-forward semantics — the broker buffers when the console is down and replays when it comes back. Inline HTTP / gRPC fail fast and rely on the publisher's retry policy.
- Existing broker investment — if the monitored app already publishes durable messages over RabbitMQ / SQS, attaching CritterWatch to the same broker is the cheapest path.
The two routes are not mutually exclusive — a service can publish telemetry to a broker and still accept control commands over HTTP. Wolverine routes each message type by its configured destination, not by transport.
Topology
Over HTTP
The monitored service exposes a single POST /_wolverine/invoke (plus a batched POST /_wolverine/batch/{queue} for fire-and-forget traffic) under the /_wolverine group prefix. CritterWatch publishes to that URL; the reply travels back inside the HTTP response body. No ReplyListener endpoint on the CritterWatch side, no broker between them.
Over gRPC
Symmetric shape — the unary Call(WolverineMessage) response carries the reply envelope. Same broker-less, response-slot-reply optimization.
Monitored-app configuration
HTTP
Add Wolverine's HTTP transport endpoint group to your ASP.NET Core routing tree at host build time:
using Wolverine.Http.Transport;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseWolverine(opts =>
{
// Your normal Wolverine + Wolverine.Http config + AddCritterWatchMonitoring.
});
var app = builder.Build();
// Add the POST /_wolverine/invoke and POST /_wolverine/batch/{queue} routes
// the HTTP transport needs. The default group prefix is "/_wolverine" — pass
// a different prefix if your ingress carves out a different path.
app.MapWolverineHttpTransportEndpoints();
app.Run();That's the only monitored-side step. The endpoints decode the inbound Wolverine envelope, route it through the normal handler graph, and emit the reply (or the batch acknowledgement) back on the HTTP response.
gRPC
The gRPC transport hosts its own embedded gRPC server — no MapGrpcService call is needed at the ASP.NET Core layer:
using Wolverine.Grpc;
var builder = Host.CreateDefaultBuilder();
builder.UseWolverine(opts =>
{
// Listen for inbound Wolverine envelopes on this gRPC port. The transport
// maps WolverineGrpcTransportService against an embedded Kestrel host.
opts.ListenAtGrpcPort(5188);
// Your normal Wolverine config + AddCritterWatchMonitoring.
});The transport's WolverineGrpcTransportService.Call(WolverineMessage) unary method is the inbound surface. The reply envelope rides back on the response of the same call — no separate reply stream.
CritterWatch-side configuration
HTTP
Configure the CritterWatch host to publish to the monitored service's /_wolverine/invoke URL:
using Wolverine.Http.Transport;
builder.Host.UseWolverine(opts =>
{
// Your normal CritterWatch.Services config.
// Publish every CritterWatch wire message to the monitored service
// over HTTP. The destination is the /_wolverine/invoke endpoint the
// monitored app's MapWolverineHttpTransportEndpoints() exposes.
opts.PublishAllMessages()
.ToHttpEndpoint("https://trip-service.internal/_wolverine/invoke");
});InvokeAsync<TResponse> then reads the reply straight off the HTTP response body:
var bus = host.Services.GetRequiredService<IMessageBus>();
var sourceCode = await bus.InvokeAsync<HandlerSourceCodeResponse>(
new RequestHandlerSourceCode("Trip:All"));No ListenAtUrl(...).UseForReplies() on the CritterWatch side. No ReplyListener wiring. The reply path is HTTP response → Wolverine envelope → awaited Task<TResponse>`.
gRPC
using Wolverine.Grpc;
builder.Host.UseWolverine(opts =>
{
// Publish every CritterWatch wire message to the monitored service
// over gRPC. The host/port pair points at the monitored app's
// ListenAtGrpcPort(...) embedded server.
opts.PublishAllMessages()
.ToGrpcEndpoint("trip-service.internal", 5188);
});Same InvokeAsync<TResponse> shape; the reply rides the unary call response slot.
How the reply gets back
The optimization that makes this story interesting is inline request/reply. Without it, a Wolverine InvokeAsync<TResponse> would need a return channel: an inbound listening endpoint configured with .UseForReplies(), plus a ReplyListener wired up to a MessageBus.Replies tracker. With the inline path:
- The HTTP transport client's
InvokeAsync(uri, envelope, ...)returns anInlineHttpReply(StatusCode, Bytes)— the receiver's reply envelope serialized straight into the HTTP response. - The gRPC transport client's unary
Call(WolverineMessage)returns the reply envelope on the same call's response slot.
Either way, the publisher reads the reply off the call's response and satisfies the Task<TResponse> directly. No listener, no broker, no correlation lookup.
That's a strict win on:
- Latency — one round trip instead of two (publish + listen-for-reply).
- Configuration surface — no inbound listener to provision on the CritterWatch side; the publisher's URL is the only configuration.
- Failure modes — the call either succeeds with the reply or fails fast; no waiting on a reply queue that might be misrouted.
WolverineRequestReplyException surfaces when the monitored service's handler throws (with the handler-side message text preserved); the regular gRPC status / HTTP status code surfaces when the transport call itself fails.
Troubleshooting the broker-less path
| What you see | Likely cause | What to check |
|---|---|---|
| Console shows the service as silent | Inline HTTP/gRPC channel doesn't buffer when the console is down. If the console was offline when telemetry was published, those batches are gone. | Confirm console availability. If you need store-and-forward semantics, run telemetry over a broker even when commands ride the inline channel. |
WolverineRequestReplyException on a command | Monitored service's handler threw. The exception text preserves the handler-side message. | Look in the service's logs around the timestamp for the corresponding handler exception. Common causes: stale tenant id, license refusal, projection in a state the command can't act on. |
HTTP 4xx / 5xx (or gRPC Unavailable / Unauthenticated) on every command | Transport-level failure: wrong URL, ingress rejecting the request, mTLS mismatch, auth filter blocking the /_wolverine group. | Curl /_wolverine/invoke from the console's network namespace and confirm the same path the publisher uses. For gRPC, grpcurl against the configured host:port. |
| Commands work, telemetry doesn't arrive | The two flows are configured independently. Telemetry may still be published to a broker the console isn't listening on. | Check the service's AddCritterWatchMonitoring URI — it's the publish destination, separate from the command-ingress route. |
| High tail-latency on InvokeAsync calls | Sync path on a service under load — the inline reply waits for the handler to finish. Long handlers stall the call. | Move long-running operations to fire-and-forget (SendAsync) and a separate result poll, or split the work into a queued workflow. |
What this channel does not do
- It is not a node registry. A clustered monitored app's leader election + per-node assignments still live in its durability table (
wolverine_nodes). HTTP / gRPC are how CritterWatch talks to a service; they don't tell CritterWatch which node owns which agent. - It is not a substitute for a broker on heavy telemetry fan-out. The inline channel is point-to-point — well-suited to control / query traffic, less well-suited to broadcasting
ServiceUpdatesto multiple independent consumers. - It does not buffer when the console is down. The publisher's retry policy handles transient failures; permanent ones fail fast. If you need store-and-forward semantics, run a broker or hybrid — broker for telemetry, inline channel for commands.
See also
- Message Flow — the broker route this page is the broker-less complement to.
- Registration — the monitored-side
AddCritterWatchMonitoringcall that hangs off the configured Wolverine transport. - Wolverine #2966 / #2990 (HTTP inline request/reply) and #2967 / #2995 (gRPC inline request/reply) — the upstream features this page consumes.
