Skip to content

Topics and patterns ​

A topic names what a message is about, as a dot-separated keypath from the general to the specific:

queues.emails.jobs.42.progress
tenants.acme.users.17.notifications
config.feature-flags

Design topics the way you would design REST paths: one segment per level of the hierarchy, with identifiers (tenant, user, job…) in their own segments. That way, a single pattern can observe a whole branch of the hierarchy.

Rules ​

RuleValue
Separator.
SegmentOne or more of A-Z a-z 0-9 _ - : %
Max length512 bytes
Max depth16 segments

Empty segments (a..b, .a, a.) are invalid. Publishing to an invalid topic, or subscribing to an invalid pattern, rejects with a RelayError (codes InvalidTopic and InvalidPattern).

The rules are enforced by the datastore (in the relay's Lua scripts and SQL functions), so every BullMQ runtime accepts and rejects exactly the same topics.

Patterns ​

Subscriptions take a topic or a pattern with wildcards:

WildcardMatchesAllowed
*Exactly one segmentIn any position
>One or more trailing segmentsOnly as the last segment
PatternMatchesDoes not match
orders.42.statusorders.42.statusorders.42, orders.42.status.x
orders.*.statusorders.42.status, orders.7.statusorders.42.items.status
orders.>orders.42, orders.42.statusorders
*.42.statusorders.42.status, invoices.42.statusorders.43.status
tenants.acme.>everything under tenants.acmetenants.other.users
>every topic

The hierarchy only exists in the names: a message published to orders.42.status is not delivered to a subscription to orders.42. Subscribe to orders.42.> to receive everything below it.

You always publish to a concrete topic: wildcards are only valid in subscriptions.

Values in topics ​

When a segment comes from data (a queue name, a user id, an email…), make sure it only contains allowed characters. Numeric ids and UUIDs are fine as they are. For anything else, percent-encode the value:

typescript
/**
 * Encodes any value as a single topic segment: only A-Z a-z 0-9 _ - and %
 * are left, so the result never contains ".", "*" or ">".
 */
function topicSegment(value: string | number): string {
  return encodeURIComponent(String(value)).replace(
    /[.!'()*~]/g,
    char => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,
  );
}

const topic = [
  'queues',
  topicSegment('emails.eu'),
  'jobs',
  job.id,
  'progress',
].join('.');
// 'queues.emails%2Eeu.jobs.42.progress'

decodeURIComponent reverses it.

WARNING

Never put unencoded user input in a topic. Besides making the topic invalid, a value containing . would create extra levels, and a subscription built from it could observe more than you intended.

Designing topics ​

  • Put the most general segment first. Wildcards then select whole branches: tenants.acme.> is everything for one tenant, tenants.*.invoices.> is the invoices of every tenant.
  • Keep identifiers in their own segment, never concatenated with other text, so * can match them.
  • Use one topic per resource (jobs.42.progress, not jobs.progress with the id in the payload) when receivers care about specific resources. Topics cost nothing until someone subscribes, and the datastore then only delivers what each receiver needs.
  • Subscribe as narrowly as you need. A wildcard subscription is convenient (one jobs.*.cancel for every queue of a process), but every matching message is written to the node's inbox, including the ones the handler then ignores. On busy topics, prefer one subscription per resource or queue the node actually handles, so the datastore does the filtering.
  • Prefer exact topics for per-resource subscriptions. A publish finds the subscriptions to its exact topic with a direct lookup, however many there are. Wildcard patterns are matched one by one against every publish whose first segment they could match, so keep their number moderate.
  • Use namespaces, not topic prefixes, to isolate environments (see Getting started).

Released under the MIT License.