What LMS Solves
Components that need to communicate sometimes can’t use standard events. They’re in different DOM trees (utility bar vs. main record page), they span Aura and LWC, or they live in separate tabs of the same console. Standard DOM events don’t cross these boundaries.
Lightning Message Service (LMS) is the cross-boundary message channel. Publish on one component, subscribe on another, regardless of where each lives in the Lightning UI.
When to Use LMS
- Cross-tab communication in Lightning Console.
- Utility bar components talking to the main record page.
- LWC ↔ Aura interop where you can’t use bubbling events.
- Loosely coupled components that shouldn’t know about each other directly.
When Not to Use LMS
- Parent/child within a single component tree — use standard events (bubbling, composed).
- Siblings in the same parent — the parent can coordinate via props and events.
- Form-level state — use a shared service or local state, not a message bus.
LMS is a global-ish message bus. Overusing it for local coordination makes the UI hard to debug.
The Message Channel
LMS is strongly-typed. You define a Message Channel metadata file describing the channel’s name and its payload shape.
<!-- force-app/main/default/messageChannels/Selected_Record__c.messageChannel-meta.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
<description>Broadcasts the currently selected record.</description>
<masterLabel>Selected Record</masterLabel>
<isExposed>true</isExposed>
<lightningMessageFields>
<description>The record Id</description>
<fieldName>recordId</fieldName>
</lightningMessageFields>
<lightningMessageFields>
<description>The source component name</description>
<fieldName>source</fieldName>
</lightningMessageFields>
</LightningMessageChannel>
Channels are first-class metadata. Version them and deploy them like any other artifact.
Publishing From LWC
import { LightningElement, wire } from 'lwc';
import { publish, MessageContext } from 'lightning/messageService';
import SELECTED_RECORD_CHANNEL from '@salesforce/messageChannel/Selected_Record__c';
export default class Publisher extends LightningElement {
@wire(MessageContext) messageContext;
handleSelect(event) {
publish(this.messageContext, SELECTED_RECORD_CHANNEL, {
recordId: event.detail.id,
source: 'Publisher'
});
}
}
The messageContext from @wire(MessageContext) is required — it’s how LMS knows which app/tab to route within.
Subscribing From LWC
import { LightningElement, wire } from 'lwc';
import { subscribe, unsubscribe, APPLICATION_SCOPE, MessageContext } from 'lightning/messageService';
import SELECTED_RECORD_CHANNEL from '@salesforce/messageChannel/Selected_Record__c';
export default class Subscriber extends LightningElement {
@wire(MessageContext) messageContext;
subscription = null;
connectedCallback() {
this.subscription = subscribe(
this.messageContext,
SELECTED_RECORD_CHANNEL,
(message) => this.handleMessage(message),
{ scope: APPLICATION_SCOPE } // or TAB_SCOPE
);
}
disconnectedCallback() {
unsubscribe(this.subscription);
this.subscription = null;
}
handleMessage(message) {
console.log('Got', message.recordId, 'from', message.source);
}
}
Three things to notice:
- Scope matters.
APPLICATION_SCOPEdelivers to all tabs in the app.TAB_SCOPE(or omit) restricts to the current tab. - Always unsubscribe in
disconnectedCallback. Omitting this leaks handlers. - Handler is a plain function. You can react however you want.
Using LMS in Aura
Aura components use a slightly different syntax:
<aura:component implements="flexipage:availableForAllPageTypes">
<lightning:messageChannel type="Selected_Record__c" onMessage="{!c.handleMessage}" scope="APPLICATION"/>
</aura:component>
The same channel is consumable from both Aura and LWC, which is the point.
Pattern 1: Cross-Tab Context Sync
Use case: user opens Account A in one tab and Account B in another. A utility bar component shows context about the currently focused tab.
Publisher: each record page publishes its recordId on tab focus. Subscriber: the utility bar subscribes with TAB_SCOPE (so it only sees messages from its app, not across).
Pattern 2: Utility-to-Main Command
Use case: a utility bar has a “Clear Filters” button. Pressing it should clear filters in whatever list view is open.
Publisher: utility bar button click publishes a ClearFilters message. Subscribers: list view components subscribe; on message, they clear their filter state.
Pattern 3: LWC Notifies Aura
Use case: modern LWC component inside a legacy Aura wrapper needs to tell the wrapper something.
Publisher (LWC): publishes a channel message. Subscriber (Aura): receives it via <lightning:messageChannel>.
This is cleaner than wrestling with component.getReference() or Aura-to-LWC event bridges.
Pattern 4: State Broadcast (Sparingly)
Use case: a global filter state — “show me only records owned by me” — needs to affect many components.
Publish on change; subscribers update their queries. Works, but get careful: if many components are subscribing and each re-fetches, you get a request thundering herd. Debounce in subscribers.
Anti-Patterns
Using LMS as props. If the components are in a parent-child relationship, use props and events, not LMS.
Untyped payloads. Every channel has a declared payload shape. Don’t stuff arbitrary data in a generic “data” field.
Forgetting to unsubscribe. Every subscribe deserves an unsubscribe. Pair them.
Broadcasting sensitive data. LMS isn’t encrypted or scoped by permission. Don’t send secrets or data the subscribing component shouldn’t see.
Chatty channels. Publishing on every keystroke or scroll event floods subscribers. Batch or debounce.
Debugging LMS
LMS doesn’t have a dedicated debugger. Tactics:
- Add temporary
console.login every subscriber to see what’s arriving. - Use the browser’s performance timeline to see publish/subscribe event flow.
- If a subscriber never fires, check the scope — mismatched scopes is a common cause.
Alternatives to LMS
Lightning Data Service. For record-level updates, LDS emits notifications that any LWC using @wire(getRecord) can receive automatically. Don’t rebuild this in LMS.
Custom Events. For components that bubble events through the same DOM tree, custom events are simpler and faster than LMS.
Pubsub Module (legacy). The older pubsub.js pattern works but is limited to a single tab. LMS supersedes it for new work.
Frequently Asked Questions
Does LMS work outside Lightning Experience?
LMS is designed for Lightning Experience and Lightning console apps. It doesn’t work in standalone non-Lightning contexts.
Can LMS persist messages?
No. Messages are ephemeral — if a subscriber starts after a message was published, it missed it.
What’s the size limit on a message payload?
There’s no hard documented limit, but keep payloads small — under a few KB. LMS is not a data transport.
Can I see all active channels?
Channels are metadata. List them from force-app/main/default/messageChannels/ or via Metadata API retrieve.