Request/reply
Sometimes you do not want to tell a node something — you want to ask it and wait for the answer: reserve this stock and tell me whether it worked, give me your current count, hand me the value for this key. That is request/reply, provided by node.Messaging.RequestAsync<TReply>.
This page shows the full round trip — the caller, the responder, timeouts, and error handling — building on the raw-wire messaging from Sending messages.
How it works
RequestAsync sends a message on the priority plane and then waits for a reply matched by CorrelationId. Every message built with CreateMessage gets a fresh correlation id; the responder must echo that same id on its reply so Nodus can pair the answer to the waiting caller.
An incoming message whose correlation id matches a pending request is delivered directly to the waiting caller, bypassing the normal handler dispatch. Everything else flows to handlers as usual.
The caller
Build a request message and await a typed reply with a timeout. The timeout parameter is optional: when you omit it or pass null, Nodus applies a default of 50 seconds. Always pass an explicit timeout appropriate to the operation — request/reply is a synchronous wait, and a long one holds the caller behind a slow or dead peer.
public sealed class ReserveRequest { public string Sku { get; set; } = ""; public int Qty { get; set; } }
public sealed class ReserveResult { public bool Ok { get; set; } public int Remaining { get; set; } }
var request = node.Messaging.CreateMessage("orders.reserve", new ReserveRequest { Sku = "ABC", Qty = 3 });
try
{
ReserveResult? result = await node.Messaging.RequestAsync<ReserveResult>(
"node-2", request, timeout: TimeSpan.FromSeconds(5));
if (result is { Ok: true })
Console.WriteLine($"reserved; {result.Remaining} left");
else
Console.WriteLine("reservation refused");
}
catch (TimeoutException)
{
Console.WriteLine("node-2 did not reply within 5s");
}
RequestAsync<TReply> deserializes the reply payload into TReply and returns it. It returns default (that is, null for a reference type) when the reply carries no payload, and it throws TimeoutException when no matching reply arrives in time.
The responder
Because the reply must carry the request's correlation id, the responder reads the raw Message. It registers a low-level IMessageHandler rather than a typed On<T>, which does not expose the correlation id. Give the handler a reference to the node so it can build and send the reply.
public sealed class ReserveHandler : IMessageHandler
{
private readonly INodusNode _node;
private readonly IInventory _inventory;
public ReserveHandler(INodusNode node, IInventory inventory)
{
_node = node;
_inventory = inventory;
}
public string Type => "orders.reserve";
public MessageDispatchMode DispatchMode => MessageDispatchMode.DataPath; // does real work
public async Task HandleAsync(Message request)
{
// 1. deserialize the request payload (via your serializer / node.Services)
var req = Deserialize<ReserveRequest>(request.Payload);
// 2. do the work
var (ok, remaining) = _inventory.TryReserve(req.Sku, req.Qty);
// 3. build the reply and ECHO the correlation id
var reply = _node.Messaging.CreateMessage("orders.reserve.reply",
new ReserveResult { Ok = ok, Remaining = remaining });
reply.CorrelationId = request.CorrelationId;
// 4. send it back to the caller on the priority plane
await _node.Messaging.SendImmediateAsync(request.SenderId, reply);
}
}
Register it during startup:
await node.Messaging.AddHandler(new ReserveHandler(node, inventory));
reply.CorrelationId = request.CorrelationId; is the load-bearing line. CreateMessage mints a new id, so if you forget to overwrite it the reply will not match the caller's pending request and the caller times out. This is the most common request/reply bug.
The reply's MessageType ("orders.reserve.reply" above) does not need a handler on the caller — the correlation match delivers it straight to the awaiting RequestAsync.
Error handling
The caller sees one of a few outcomes. Handle each explicitly rather than letting a request hang.
| Situation | What the caller sees | What to do |
|---|---|---|
| Reply not received in time | TimeoutException | Retry, fail the operation, or fall back — do not ignore it. |
| Reply has no payload | RequestAsync returns default/null | Treat as a null result; distinguish it from a valid empty reply if needed. |
| Responder threw while handling | No reply is sent, so the caller times out | Have the responder catch and send an explicit error reply (below). |
| Target peer left mid-request | Send throws or times out | Catch and re-route to another member or the current leader. |
For application-level errors, prefer sending an explicit failure reply over letting the caller time out. A timeout is slow and ambiguous, whereas a failure reply is immediate and specific.
public async Task HandleAsync(Message request)
{
try
{
var req = Deserialize<ReserveRequest>(request.Payload);
var (ok, remaining) = _inventory.TryReserve(req.Sku, req.Qty);
await Reply(request, new ReserveResult { Ok = ok, Remaining = remaining });
}
catch (Exception)
{
// surface failure as a reply, not a timeout
await Reply(request, new ReserveResult { Ok = false, Remaining = -1 });
}
Task Reply(Message req, ReserveResult r)
{
var reply = _node.Messaging.CreateMessage("orders.reserve.reply", r);
reply.CorrelationId = req.CorrelationId;
return _node.Messaging.SendImmediateAsync(req.SenderId, reply);
}
}
When you retry a request after a timeout, remember that the first attempt may still have been processed. Make the operation idempotent — for example, key the reservation by a client-supplied request id — so a retry cannot double-apply.
When to use request/reply versus events
Request/reply couples the caller to one responder for the duration of the call. Keep the work short, always set a timeout, and prefer explicit failure replies. Use it when you need an answer from a specific node; use the other options when you do not.
| Need | Use |
|---|---|
| Ask one node, wait for its answer | RequestAsync<TReply> |
| Tell a node something, no answer needed | SendAsync<T> |
| Announce to whoever is interested | node.Events.PublishAsync |
Next steps
- Sending messages — the fire-and-forget half of messaging.
- Messaging — the transport and correlation model.
- Health and membership — pick a live target for a request.