Skip to content

Request/reply

Request/reply is Matrix's primary call idiom. The caller publishes one envelope to the target's $inbox and awaits one response on a unique reply subject. From the caller's perspective it is synchronous (await); on the wire it is two pub/sub messages with a correlation ID linking them.

The shape

The envelope (projects/matrix-3/packages/core/src/engine/messaging/MxEnvelope.ts):

typescript
export interface MxEnvelope {
  op: string;                 // operation name, e.g. 'registry.resolve'
  payload: unknown;           // the call arguments
  iface?: string;             // contracted interface name
  correlationId?: string;     // unique per request
  replyTo?: string;           // reply subject
  lamport?: number;           // logical clock
  from?: string;              // sender mount path
  traceId?: string; spanId?: string; parentSpanId?: string;  // tracing
}

A request envelope sets op, payload, correlationId, and replyTo. A response envelope (delivered to replyTo) sets op: '$reply', the result in payload, and echoes the correlationId.

Wire flow

Subjects (root = COM.NIMBLETEC.RICHARD-SANTOMAURO):

1. caller subscribes      → COM.NIMBLETEC.RICHARD-SANTOMAURO.$reply.f12d-7af0
2. caller publishes       → COM.NIMBLETEC.RICHARD-SANTOMAURO.system.registry.$inbox
                            { op: 'registry.resolve',
                              correlationId: 'f12d-7af0',
                              replyTo: '$replies.f12d-7af0',
                              payload: { logicalMount: 'chat.conversation' } }
3. registry actor handles → publishes response on
                            COM.NIMBLETEC.RICHARD-SANTOMAURO.$reply.f12d-7af0
                            { op: '$reply', correlationId: 'f12d-7af0',
                              payload: { ok: true, providers: [...] } }
4. caller's subscription delivers the response, then unsubscribes

The transport accepts $replies.f12d-7af0 (semantic) and emits {root}.$reply.f12d-7af0 (wire). See projects/matrix-3/packages/core/src/transport/NatsTransport.ts lines 267–305.

NATS-protocol vs Matrix-form replies

NATS itself supports request/reply natively via the replyTo field on a message header. When the underlying transport is NATS and the connection exposes a request() method, RequestReply.execute may use the native NATS _INBOX.* reply subject for performance — a single subscription, no explicit reply pub.

When the transport does not expose request() (or for cross-root calls), the request/reply path falls back to the explicit $reply.{cid} form documented above. Both are interoperable: the actor handler does not know or care which form was used.

Note: the source comment in RequestReply.ts line 318 explains: "Do not use natsClient.request() here: protocol reply inboxes (_INBOX.*) bypass our root-prefixed subjects and break ACL enforcement." The Matrix request path uses our $reply.{cid} form so every subject is root-prefixed and authorizable.

Default timeout

The default request timeout is 5000 ms (RequestReply.ts line 263: const timeoutMs = opts.timeoutMs ?? 5000;). Callers can override per request:

typescript
const result = await actor.invoke(targetMount, op, payload, { timeoutMs: 30_000 });

When the timeout fires, the caller unsubscribes from the reply subject and the call rejects with a timeout error. See Timeouts for the full classification.

When to use request/reply

Use request/replyUse events instead
The caller needs the answer to proceedThe caller is broadcasting state changes
One specific actor must handle the callAnyone interested may listen
The op modifies state (write)The op is a notification (read-mostly)
The result is small enough to fit in one envelopeThe output is a stream

If the answer is multi-message (streaming, paginated, progress events), use a session on top of an initiating request/reply.

Idempotency

Per CLAUDE.md Rule 9: "Every write op accepts optional idempotencyKey." Callers that retry on timeout should pass the same idempotencyKey so the actor can deduplicate. Idempotency policy is per-actor; the framework does not auto-deduplicate.

What request/reply does NOT do

  • It does not provide ordering guarantees across calls. Two parallel requests to the same actor may complete in any order.
  • It does not retry. If the request times out or the actor returns an error, the caller decides whether to retry.
  • It does not deliver the same response twice. The reply subject is unsubscribed after the first message arrives.
  • It does not coordinate across multiple actors. Multi-actor workflows (sagas, two-phase commit) build on top of request/reply with explicit coordination ops.

See also