Skip to content

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:

  1. Caller generates a unique CID per request.
  2. Caller subscribes on $reply.<cid> (or uses NATS-native reply subject).
  3. Caller publishes the request envelope with correlationId: <cid> and replyTo: '$replies.<cid>'.
  4. Actor produces a response with op: '$reply' and the same correlationId.
  5. Actor publishes on the replyTo subject.
  6. Caller receives the response, matches correlationId, resolves its await, 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-04ca15a3b6ab

The 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 _splitPathTokens will 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 timeout

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

ConceptFieldLifetimeWhere it lives
Correlation IDcorrelationId (envelope)one round-tripheader field of every request/reply pair
Session IDsessionId (payload, by convention)many round-trips + eventsinside 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