Broadcast Events
Use whenevery running service must react to the same message (cache invalidation, feature-flag flips, config reload).
You getper-service durable consumers on a shared stream, late-joining replicas catch up automatically.
A broadcast event fans out: every service with a matching handler gets its own copy. A workqueue event reaches one instance, and an ordered event gives each instance a sequential replay.
A typical case: a feature flag changes, and orders, payments and notifications all have to refresh their local cache. Calling each service does not scale, and a workqueue event would reach only one of them.
How it works
- Publish: a service calls
client.emit('broadcast:config.updated', data). Thebroadcast:prefix marks a fan-out event. - Route: the transport publishes to
broadcast.config.updated, a global subject with no service scope. - Store: the message is stored in the single
broadcast-streamthat all services share. It usesLimitsretention, so acks do not delete messages; they age out bymax_age,max_msgsormax_bytes. That is what lets a new instance catch up on recent broadcasts. - Consume: each service with a
{ broadcast: true }handler has its own durable consumer on the stream, and each consumer receives the message. - Settle: each service decodes, handles, and acks or naks on its own.
Sending and handling
Publish with the broadcast: prefix:
import { Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { lastValueFrom } from 'rxjs';
@Injectable()
export class AdminService {
constructor(@Inject('admin') private readonly client: ClientProxy) {}
async updateFeatureFlag(key: string, enabled: boolean): Promise<void> {
await this.featureFlagRepository.update(key, enabled);
await lastValueFrom(this.client.emit('broadcast:feature-flag.updated', { key, enabled }));
}
}
Handle with the bare pattern and { broadcast: true }. Every service that declares this handler receives the message through its own consumer:
import { Controller } from '@nestjs/common';
import { EventPattern, Payload } from '@nestjs/microservices';
@Controller()
export class OrdersController {
@EventPattern('feature-flag.updated', { broadcast: true })
handleFeatureFlag(@Payload() data: { key: string; enabled: boolean }): void {
this.featureFlagService.refresh(data.key, data.enabled);
}
}
The broadcast: prefix belongs only on the sending side. The handler pattern is 'feature-flag.updated' with { broadcast: true } in extras: the prefix controls routing, the extras flag controls consumer registration.
Each consumer filters on the broadcast subjects its service has handlers for, so a service receives only the broadcasts it handles.
Delivery semantics
Every outcome in workqueue delivery semantics applies, per consumer: retries, the retry delay curve, dead-lettering and termination all happen for one service at a time. Delivery is at-least-once per subscribing service.
When the orders service fails a message:
- only the orders consumer redelivers it, and payments and analytics are unaffected;
- each consumer counts its own delivery attempts, so orders can dead-letter a message that payments processed fine;
- a bug or an outage in one service never blocks the others.
Configuration: shared stream, per-service consumers
Broadcast settings live under broadcast in forRoot(), at two scopes:
JetstreamModule.forRoot({
name: 'orders',
servers: ['nats://localhost:4222'],
broadcast: {
// Shared: applies to broadcast-stream for every service
stream: { max_age: toNanos(48, 'hours') },
// Per service: only the orders broadcast consumer
consumer: { max_deliver: 5, ack_wait: toNanos(30, 'seconds') },
retry: [1_000, 5_000],
},
});
broadcast.streamchanges the one stream everybody shares.max_age(1 hour by default) is the catch-up window for a new instance.broadcast.consumer,broadcast.retryandbroadcast.concurrencyapply only to this service's consumer,{service}__microservice_broadcast-consumer. Payments can retry ten times while orders retries five.
Any service can change the shared stream on startup, and the last one to start wins for mutable properties. Let one service own the stream settings and leave broadcast.stream unset everywhere else.
The transport logs each change it applies, so drift shows up in the logs. A conflict on an immutable property such as storage only logs a warning unless provisioning.allowDestructiveMigration is on.
Delayed broadcasts with scheduleAt() need broadcast.stream.allow_msg_schedules: true, separate from the event stream flag. See Scheduling.
Defaults for both scopes are in Default Configs, and mutable versus immutable properties in Immutable vs mutable.
Cache invalidation example
// Publisher
await lastValueFrom(
this.client.emit('broadcast:cache.invalidate', { entity: 'product', id: productId }),
);
// Handler, in every service that caches products
@EventPattern('cache.invalidate', { broadcast: true })
handleCacheInvalidation(@Payload() data: CacheInvalidationEvent): void {
if (data.entity === 'product') {
this.productCache.delete(data.id);
}
}
Config propagation and feature-flag toggles follow the same shape: publish once with the prefix, handle in each service with { broadcast: true }.
See also
- Performance Tuning: broadcast and workqueue consumers share the concurrency budget
- Dead Letter Queue: per-consumer dead letters
- Edge Cases: why the stream has no service prefix