Skip to content

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 upRabbitMQ / SQS / Azure Service Bus queueNone — uses the service's existing HTTP or gRPC ingress
Where commands goQueue, with broker fan-outDirect point-to-point call to the service
Console-side listenerRequired (reply queue)None — replies ride the HTTP / gRPC response slot
Console outageTelemetry buffers in the broker; replays on reconnectPublisher's retry policy only; persistent outage = lost telemetry
Failure shapeQueue depth grows, then alertsHTTP/gRPC status surfaces on the console call site immediately
Best forHeavy telemetry, multi-consumer fan-outSingle-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 ServiceUpdates snapshot 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:

csharp
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:

csharp
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:

csharp
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:

csharp
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

csharp
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&lt;TResponse&gt; 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&lt;TResponse&gt; 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 an InlineHttpReply(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&lt;TResponse&gt; 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 seeLikely causeWhat to check
Console shows the service as silentInline 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 commandMonitored 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 commandTransport-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 arriveThe 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 callsSync 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 ServiceUpdates to 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 AddCritterWatchMonitoring call 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.

Released under the MIT License.