Skip to content
Back to the Lab

Talking Between Tabs Without a Server

A deep technical guide to the Broadcast Channel API, covering why localStorage became an unofficial messaging channel before this existed, creating and using a channel, what can be sent through structured clone, why the sender never hears its own message, scope and closing, a logout example, and how it contrasts with the Web Locks API.

A dark Egnworks banner representing one message reaching several open browser tabs at once.

Keeping several open tabs of the same application in sync, a login in one tab logging the others out, a new item added in one tab appearing in another without a refresh, has always needed some way for tabs to talk to each other directly. For a long time the only real option was a hack, listening for the storage event that fires whenever localStorage changes anywhere on the origin, and using it as an accidental messaging channel it was never actually designed to be. The Broadcast Channel API gives tabs a real, purpose built channel instead.

Why localStorage Became an Unofficial Messaging Channel#

The storage event fires on every other tab of the same origin whenever a localStorage key actually changes value, and critically, it never fires on the tab that made the change itself. That second property happens to be exactly what a lot of cross tab messaging needs, notify every other tab, not the one that triggered the notification, which is precisely why so many projects repurposed it for messaging long before a real messaging primitive existed. The catch is that it only fires on an actual value change, writing the same value twice in a row fires nothing the second time, and every message has to be encoded into a string and stashed in actual persistent storage just to trigger an event that has nothing to do with storage at all.

Creating a Channel#

A BroadcastChannel is created with a name, and any other BroadcastChannel created with that same name on the same origin, in any tab, window, iframe, or worker, is now connected to it.

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

No handshake or registration step is needed beyond constructing the object with a matching name, the browser handles connecting every instance sharing that name automatically.

Sending and Receiving a Message#

postMessage() sends data to every other connected channel, and a message event delivers it on the receiving end.

// tab A
channel.postMessage({ type: "logout" });

// tab B
channel.addEventListener("message", (event) => {
  if (event.data.type === "logout") {
    redirectToLogin();
  }
});

Every tab that constructed a BroadcastChannel with the name "app-events" receives this message, there is no concept of addressing one specific tab out of several, a broadcast reaches everyone connected to that channel name.

What Can Be Sent: Structured Clone#

postMessage() accepts more than plain strings, the data is serialized using the structured clone algorithm, the same mechanism postMessage on a Worker or window already uses. Objects, arrays, Map, Set, Date, and typed arrays all survive the trip intact, without needing to be manually serialized into JSON first, though functions, DOM nodes, and a handful of other non-cloneable types still cannot cross this boundary.

The Sender Never Hears Its Own Message#

A channel instance never receives a message it posted itself, only other instances connected under the same name do. This is not an accident of implementation, it is the behavior the API is specified to have, and it is what makes the API fit the same broadcast-to-everyone-else pattern the storage event hack approximated by accident. A tab that needs to react to its own action does not need the event at all, it can simply run that reaction directly at the call site, the channel exists specifically for informing everyone else.

Multiple Channels Coexisting Under Different Names#

An application is not limited to one channel, separate channel names create entirely independent broadcast groups that never interfere with each other.

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

This is a reasonable way to keep unrelated categories of cross tab messaging from having to share one event handler that filters messages by type, when the channel name itself can already do that separation.

Scope: Same Origin, Not Just Same Site#

Broadcast Channel connects contexts sharing the same origin, and modern storage partitioning further scopes this to the same top level site, an iframe from one origin embedded inside a different top level site cannot reach a channel with the same name opened on that origin’s own standalone page elsewhere. This mirrors the same-origin boundary most other browser storage and messaging primitives already enforce, there is no mechanism here for communicating across genuinely different origins.

Closing a Channel#

close() disconnects a channel instance, after which it stops receiving messages and becomes eligible for garbage collection.

channel.close();

A component that creates a channel when it mounts should close it when it unmounts, the same discipline already applied to removing an event listener, otherwise the channel instance keeps a live connection open for the lifetime of the page even after nothing is using it anymore.

A Practical Example#

Logging a user out in every open tab the moment they log out in any one of them is a natural fit for a broadcast, since every tab genuinely needs to react the same way.

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

function logout() {
  clearSession();
  authChannel.postMessage({ type: "logout" });
  redirectToLogin();
}

authChannel.addEventListener("message", (event) => {
  if (event.data.type === "logout") {
    clearSession();
    redirectToLogin();
  }
});

The tab where the user actually clicked logout runs its own redirect directly, and every other open tab runs the same cleanup in response to the broadcast, without either tab needing to know how many other tabs exist.

Broadcast Channel Versus the Web Locks API#

Broadcast Channel and the Web Locks API solve adjacent but different coordination problems. A broadcast reaches every listening tab at once, with no concept of ownership or exclusivity, while a lock grants access to exactly one holder at a time and says nothing to anyone else until that holder is done. The two combine naturally, a tab that wins a lock to become the sole tab responsible for some background task can broadcast that fact to the others, so every tab knows who currently holds responsibility without each of them needing to query the lock manager directly.

What It Does Not Do#

A message posted while a particular tab is closed is simply never received by that tab, there is no queue or replay of missed messages waiting for a channel that opens later. Broadcast Channel is also strictly a same-browser-profile mechanism, it says nothing about a second device signed into the same account, that still requires a server pushing updates through its own separate channel such as a WebSocket or WebTransport connection.

Browser Support#

The Broadcast Channel API is supported in Chrome, Edge, Firefox, and Safari, and counts as broadly available today, safe to use directly without a fallback for a project targeting current browser versions.

Conclusion#

The Broadcast Channel API replaces a localStorage event hack that happened to work for cross tab messaging with a primitive actually designed for it. postMessage() and the message event cover sending and receiving, structured clone removes the need for manual serialization, and the sender never receiving its own message is exactly the behavior most broadcast use cases already want. Paired with the Web Locks API for the cases that need exclusivity rather than a broadcast, these two cover most of what cross tab coordination on the same origin actually requires.

References#

MDN: Broadcast Channel API

MDN: BroadcastChannel

MDN: The Structured Clone Algorithm

Can I Use: Broadcast Channel