Area: Messaging between tabs
Related: Browser APIs (map), Web Locks API, Web Workers, IndexedDB

What is it?

BroadcastChannel is a named message bus shared by every tab, iframe and worker of the same origin. Anyone can post to a channel name, and everyone else listening on that name gets the message.

const channel = new BroadcastChannel("auth");
 
channel.onmessage = (event) => {
  if (event.data.type === "signed-out") showSignedOutScreen();
};
 
channel.postMessage({ type: "signed-out" });

A walkie-talkie channel for one website. Everyone tuned to “auth” hears it. Nobody outside the site can tune in.

Why it’s needed

Tabs of the same site share storage (IndexedDB, localStorage, cookies) but not memory. Each tab has its own copy of the app’s state. When one tab changes something, the others don’t know.

Typical cases:

  • You sign out in one tab. The other five tabs still show your private data.
  • You rename a note in one tab. Other tabs show the old name.
  • A tab writes to IndexedDB. Other tabs have a stale in-memory cache.

IndexedDB has no change events, so you need a way to say “something changed”. BroadcastChannel is that way.

The details that matter

  • Same origin only. https://a.com and https://b.com can’t talk. In third-party iframes, the channel is also partitioned by the top-level site, like other storage.
  • The sender doesn’t receive its own message. Other BroadcastChannel objects with the same name in the same tab do receive it. Only the exact object that posted is skipped.
  • Messages use structured clone, the same as Web Workers and IndexedDB. Objects, Dates, Maps, Blobs are fine. Functions throw.
  • No queue and no history. If no tab is listening at the moment you post, the message is gone. A tab that opens later never sees earlier messages.
  • No delivery guarantee across a crash and no reply. It’s fire-and-forget. If you need an answer, the receiver has to post a reply message with an id you sent.
  • Order: messages from one sender arrive in the order they were sent. There’s no global order between different senders.
  • channel.close() stops listening and lets it be garbage collected. Close channels in component cleanup, or you’ll leak listeners.
  • Support: all modern browsers. Safari added it late (15.4). Code for older webviews should check typeof BroadcastChannel !== "undefined".

Design rule: send “something changed”, not the data

The common mistake is sending the full new state:

// ❌ fragile
channel.postMessage({ type: "note-updated", note: fullNoteObject });

Problems: the message can be large, the receiving tab may have a newer version already, messages from different tabs can cross each other, and a tab that missed a message has no way to catch up.

Better:

// ✅ better
channel.postMessage({ type: "note-changed", noteId: "n1" });
 
channel.onmessage = async ({ data }) => {
  if (data.type === "note-changed") {
    const fresh = await readNoteFromIndexedDb(data.noteId); // the real source of truth
    updateUi(fresh);
  }
};

The message is just a ping. The receiver re-reads from the shared store. That handles out-of-order messages, duplicates, and big data for free. It’s the same idea as cache invalidation on a server: say “this key is stale”, don’t push the new value.

It also makes messages idempotent. Receiving the same ping twice just means re-reading twice.

Other ways to talk between tabs, compared

MechanismHowDownsides
BroadcastChannelnamed busno history, no reply
storage eventwrite to localStorage, other tabs get a storage eventstrings only, synchronous storage, fires only for other tabs, an old trick
window.postMessagesend to a specific window you have a reference toneeds a handle (window.opener, an iframe); used for cross-origin talk with a target origin check
MessageChannela private two-way pipe with two portsyou have to pass a port over first; great for worker RPC
SharedWorkerone shared worker all tabs connect topatchy support historically; more setup
Service worker clients.matchAll()the service worker posts to every tabneeds a service worker

Before BroadcastChannel existed, libraries used the storage event trick: write a random value to a key, and other tabs listen for changes to that key. You’ll still see it in old code.

Combining it with Web Locks

Web Locks API decides who does something. BroadcastChannel tells everyone what happened.

Leader election example: the tab that wins the leader lock opens the single WebSocket. When a message arrives from the server, the leader posts it on a BroadcastChannel so every tab updates. When the leader closes, another tab gets the lock and takes over the socket.

Security

  • Only your own origin can post, so you don’t need to check event.origin like you do with window.postMessage.
  • Still validate the shape of messages. An old tab running last week’s code might send an older message format. Ignore types you don’t know instead of crashing.
  • Don’t send secrets you wouldn’t want in other tabs. Any same-origin script, including a third-party script loaded on your page, can listen.

A real-world example

A notes app that people keep open in many tabs uses BroadcastChannel for three things:

  • Sign-in and sign-out across tabs. The auth library’s account events carry the signal. Each receiving tab decides how to react: show the signed-out state, or pick up the new account.
  • Local content changes. When a tab saves a note body to the local store, it posts “content changed” on a channel, so other tabs refresh their copies. It checks that BroadcastChannel exists first.
  • Search index. The search worker refreshes its index when another tab reports changes, so search results stay current in every tab.

Gotchas

  • You don’t get your own message. If the posting tab should also react, call the handler directly too.
  • A tab opened after the message never hears it. On startup, read the current state from storage.
  • Close the channel when a component unmounts.
  • Don’t send full objects. Send ids and re-read.
  • Messages from an older deploy may have a different shape. Ignore unknown types.
  • It doesn’t cross browsers, devices or profiles. For that you need a server.

Practice

  1. Two tabs, one counter in localStorage. Increment in one, update the other with BroadcastChannel. Then do the same with the storage event and compare.
  2. Build “sign out everywhere”: a sign-out in any tab blanks every tab.
  3. Post 1,000 messages quickly from two tabs at once and check what order each tab receives them in.
  4. Combine with Web Locks: elect a leader that ticks a timer and broadcasts the tick to all tabs.

Sources worth reading

  • MDN: Broadcast Channel API
  • HTML spec: Broadcasting to other browsing contexts
  • Chrome Developers: BroadcastChannel API: a message bus for the web