jsguides

Using the Broadcast Channel API for Cross-Tab Messaging

The Broadcast Channel API lets tabs, windows, iframes, and web workers on the same origin send messages to each other. It is a simple alternative to postMessage when you do not need to target a specific window — any context listening on the same channel receives the message.

Basic Usage

Create a channel and start sending messages:

const channel = new BroadcastChannel("app-state");

// Listen for messages
channel.onmessage = (event) => {
  console.log("Received:", event.data);
};

// Send a message to all other contexts on this channel
channel.postMessage({ type: "user-login", userId: 12345 });

Every context that creates a BroadcastChannel("app-state") with the same name joins the same channel. Messages sent from one context reach all other contexts on that channel. This is a one-to-many model: one sender, many receivers, with no need to track window references.

When you are done listening, call close() to disconnect from the channel and stop receiving messages.

Closing a channel

const channel = new BroadcastChannel("sync");
channel.postMessage({ type: "data-update", payload: { count: 42 } });
channel.close();  // stop receiving messages

After close(), the channel is disconnected. It cannot be reopened — create a new instance if needed. Calling close() is important during cleanup to prevent memory leaks from lingering listeners.

A common real-world use for BroadcastChannel is keeping application state consistent across multiple open tabs. When one tab changes a setting, all others can react to the update without polling.

Cross-tab state synchronization

Use a broadcast channel to keep multiple tabs in sync:

// In each tab
const stateChannel = new BroadcastChannel("app-state");

// Store current state
let appState = { theme: "light", sidebar: true };

// When state changes, broadcast to other tabs
function updateState(patch) {
  appState = { ...appState, ...patch };
  stateChannel.postMessage({
    type: "state-update",
    state: appState,
    timestamp: Date.now()
  });
}

// Receive state updates from other tabs
stateChannel.onmessage = (event) => {
  if (event.data.type === "state-update") {
    appState = event.data.state;
    renderApp();
  }
};

When the user changes theme in tab A, tab B and tab C receive the update and re-render automatically. The channel acts as a shared bus: any tab can publish a state change, and all others pick it up immediately. This same pattern works for keeping shopping cart counts, notification badges, or dark-mode preferences in sync across every open tab for your origin.

Communicating with web workers

// main.js (main thread)
const workerChannel = new BroadcastChannel("worker-messages");

workerChannel.onmessage = (event) => {
  console.log("Worker says:", event.data);
};

// Send work to the worker
const worker = new Worker("processor.js");

worker.onmessage = (event) => {
  // This is standard postMessage
  workerChannel.postMessage({ result: event.data });
};

The main thread uses BroadcastChannel to receive messages from other contexts and to redistribute worker results. But the worker itself does not use BroadcastChannel — workers communicate with the main thread through the dedicated postMessage channel on the worker object.

Inside the worker, the same pattern applies in reverse: it listens on self.onmessage for work from the main thread and sends results back through self.postMessage.

// processor.js (worker)
self.onmessage = (event) => {
  // Send result back via standard postMessage
  // The main thread broadcasts it to other contexts
  self.postMessage({ processed: event.data.value * 2 });
};

Note: the Broadcast Channel and postMessage between main thread and workers are separate APIs. Messages from workers arrive via onmessage on the worker, not via BroadcastChannel. Use the broadcast channel to redistribute those messages to other contexts.

Message Format

Any structured-cloneable value works as the message payload:

channel.postMessage("simple string");
channel.postMessage({ key: "value", numbers: [1, 2, 3] });
channel.postMessage(new Uint8Array([0x68, 0x69]));  // ArrayBuffer views
channel.postMessage(null);  // works too

Comparing communication APIs

APIUse when
BroadcastChannelBroadcasting to all contexts on the same origin
postMessageSending to a specific window or worker
MessageChannelPoint-to-point communication between two specific contexts
localStorage eventReacting to storage changes across tabs

BroadcastChannel is simpler than postMessage when you need fan-out: one message reaches everyone listening. postMessage requires keeping a reference to the target window.

Authentication state sync example

Keep authentication state consistent across tabs:

const authChannel = new BroadcastChannel("auth-events");

// Simulated auth check
let currentUser = null;

function login(user) {
  currentUser = user;
  authChannel.postMessage({
    type: "login",
    user: user,
    at: Date.now()
  });
}

function logout() {
  currentUser = null;
  authChannel.postMessage({
    type: "logout",
    at: Date.now()
  });
}

// React to auth events from other tabs
authChannel.onmessage = (event) => {
  const { type, user } = event.data;

  if (type === "login") {
    currentUser = user;
    updateUI();
  } else if (type === "logout") {
    currentUser = null;
    showLoginScreen();
  }
};

When a user logs out in tab A, all other open tabs receive the broadcast and update their UI accordingly.

Limitations

Same origin only. Channels cannot communicate across origins. If you need cross-origin messaging, postMessage with a target origin is the correct tool.

No delivery confirmation. postMessage delivers asynchronously with no acknowledgment. If you need confirmation of delivery, implement a reply protocol yourself.

Browser support. BroadcastChannel is supported in all modern browsers. Internet Explorer does not support it. For IE support, use a localStorage-based fallback or a library.

When BroadcastChannel fits best

BroadcastChannel works best when the same origin has multiple live contexts that should react to the same state change. Think about tabs that need to keep a cart count in sync, dashboard windows that show the same live data, or workers that need a lightweight way to announce a result. In those cases, the API is much simpler than building a custom messaging layer on top of storage events or polling. The big advantage is that one message can reach every listener without any extra routing code.

The downside is that it only solves one class of problem. It does not help you reach another origin, and it does not store history for late joiners. That means you still need a source of truth somewhere else if new tabs should reconstruct state after opening. A common pattern is to combine BroadcastChannel with local persistence: store the latest snapshot in localStorage or IndexedDB, then use the channel for live updates. That gives you fast fan-out and a way to recover after reloads.

Designing messages well

Broadcast messages should be small and predictable. A message type string plus a payload object is usually enough. When the shape stays consistent, each receiver can switch on the type and decide what to do without guessing. That makes the channel easier to extend later, because new message kinds can arrive without forcing every listener to rewrite its logic. It also helps to include a timestamp or version field when different tabs may process updates in different orders.

Keep in mind that listeners may not all be in the same state. One tab might be on a settings page while another is showing a list, so a message should describe the change rather than assume a specific UI. That approach makes handlers more durable and less tied to a single screen. If a message needs confirmation, send a separate reply message or fall back to a request-response flow over postMessage. BroadcastChannel is best when the sender wants to announce something, not when it needs one exact answer back.

Reliability And Cleanup

Because channels stay open until you close them, cleanup matters. A page that creates a channel in response to mounting or navigation should close it when the page no longer needs updates. That prevents old listeners from hanging around and reacting to messages after their UI has disappeared. It also keeps memory use predictable in applications that open and close many views over time.

There is another small habit that pays off: create the channel once per logical feature, not once per function call. Reusing the same instance keeps the code easier to reason about and avoids duplicate listeners. If you notice multiple parts of the app each creating their own channel with the same name, consider moving that responsibility into one shared module. That gives you one place to manage the channel name, message shape, and teardown behavior.

A simple mental model

Think of BroadcastChannel as a loudspeaker inside one origin. Anyone who is listening hears the message, and nobody else does. That picture makes it easier to reason about where the message should go and what should happen if no one is listening yet. If the app needs history, storage, or cross-origin reach, the broadcast channel is only one part of the solution.

A practical benefit of the named-channel model is that each channel name can serve as a shared convention across features. When every tab and worker agrees on a channel like "app-state" or "auth-events", you get one clear route for live coordination instead of ad-hoc messaging paths. That naming discipline also helps during debugging: you can trace exactly which parts of the application share which message flow.

See also