Appearance
Correlation IDs
A correlation ID (CID) is a short opaque token that links a request envelope to its response. It is the field MxEnvelope.correlationId.
Why CIDs exist
NATS pub/sub is broadcast-fanout. If two callers send requests to the same actor inbox at almost the same time, the actor publishes two responses. The caller needs a way to know which response belongs to which request. The correlation ID is that link:
- Caller generates a unique CID per request.
- Caller subscribes on
$reply.<cid>(or uses NATS-native reply subject). - Caller publishes the request envelope with
correlationId: <cid>andreplyTo: '$replies.<cid>'. - Actor produces a response with
op: '$reply'and the samecorrelationId. - Actor publishes on the
replyTosubject. - Caller receives the response, matches
correlationId, resolves itsawait, and unsubscribes.
Generation
CIDs are generated client-side. There is no central authority. The framework helpers in RequestReply use opaque short tokens; production code typically uses random hex strings, nanoids, or UUIDs. Concrete shapes seen in the running system:
f12d-7af0
abc-123
0c7b04e9-a9ce-4c41-bb7f-04ca15a3b6abThe format is not load-bearing — only uniqueness matters. The transport includes the CID verbatim in the wire subject, so it must be a valid NATS token (alphanumeric, hyphens, underscores; no dots or wildcards).
Caution: if you put a
.in a CID, the transport's_splitPathTokenswill split it across NATS subject tokens. The reply subject grammar is{root}.$reply.<single-token>. Use hyphens or underscores instead of dots.
Reply subject form
{root}.$reply.<cid>Semantic equivalent: $replies.<cid> (note the plural — historical, kept for transport compatibility).
typescript
// NatsTransport.semanticToNats — projects/matrix-3/packages/core/src/transport/NatsTransport.ts:298
if (topic.startsWith(replyPrefix)) {
const correlationId = topic.slice(replyPrefix.length);
if (correlationId.length === 0) {
throw new Error('NatsTransport: $replies topic must include correlation id');
}
// ...
}The transport requires a non-empty CID; an empty $replies. is an error.
Interop with NATS native reply
NATS itself supports request/reply via the replyTo field on a message header. Some clients (e.g., the official nats.js library) use a private _INBOX.* subject for that. When the underlying transport exposes request(), RequestReply.execute MAY use it for performance, but production code does not. From RequestReply.ts line 318:
"Do not use natsClient.request() here: protocol reply inboxes (_INBOX.*) bypass our root-prefixed subjects and break ACL enforcement."
The Matrix request path always publishes its own $reply.<cid> subscription under the caller's root. This keeps every subject root-prefixed, which means NATS account ACL can authorize them — _INBOX.* lives in the system account namespace and would bypass that.
Lifecycle and cleanup
A reply subscription has a tightly bounded lifetime:
1. caller subscribes to $reply.<cid>
2. caller publishes request
3. timer starts (default 5000 ms — see RequestReply.ts:263)
4. EITHER reply arrives → unsubscribe → resolve
OR timer fires → unsubscribe → reject with timeoutThere is never a long-lived reply subscription. If the caller doesn't get a reply within the timeout, the subscription is torn down regardless.
CIDs are not session IDs
A correlation ID is request-scoped. It links one request to one reply. A session ID is conversation-scoped — it links many messages together. The two never alias:
| Concept | Field | Lifetime | Where it lives |
|---|---|---|---|
| Correlation ID | correlationId (envelope) | one round-trip | header field of every request/reply pair |
| Session ID | sessionId (payload, by convention) | many round-trips + events | inside the payload of session-aware ops |
A streaming session uses one initiating request/reply (with its own CID) to establish the session, then events tagged with sessionId for the duration. See Streaming / sessions.
CIDs across federation
For cross-root requests, the CID is generated by the caller and the reply subject lives in the caller's root. The target's transport publishes the response on the caller's root reply subject — this requires NATS account permissions on the federation hub.
caller root: COM.NIMBLETEC.RICHARD-SANTOMAURO
target root: AI.HIVECAST.HOST
request: AI.HIVECAST.HOST.system.gateway.http.$inbox
reply: COM.NIMBLETEC.RICHARD-SANTOMAURO.$reply.<cid>The replyTo field in the envelope contains the absolute reply subject (COM.NIMBLETEC.RICHARD-SANTOMAURO/$replies.<cid> in semantic form). The target's transport reads this, builds the wire subject, and publishes.
See also
- Request envelope — the field that carries the CID.
- Response envelope — the matching reply.
- Subjects —
$reply.{cid}wire format. - Subject grammar — formal rules.
- Sessions — session IDs vs CIDs.