diff --git a/lib/tunnelbroker/secondary-tunnelbroker-connection.js b/lib/tunnelbroker/secondary-tunnelbroker-connection.js
new file mode 100644
--- /dev/null
+++ b/lib/tunnelbroker/secondary-tunnelbroker-connection.js
@@ -0,0 +1,19 @@
+// @flow
+
+import type { MessageToDeviceRequest } from '../types/tunnelbroker/message-to-device-request-types.js';
+
+type RemoveCallback = () => void;
+
+export type SecondaryTunnelbrokerConnection = {
+  // Used by an inactive tab to send messages
+  sendMessage: MessageToDeviceRequest => mixed,
+  // Active tab receives messages from inactive tabs
+  onSendMessage: ((MessageToDeviceRequest) => mixed) => RemoveCallback,
+
+  // Active tab sets the message status of messages from inactive tabs
+  setMessageStatus: (messageID: string, error: ?string) => mixed,
+  // Inactive tabs receive message status and resolve or reject promises
+  onMessageStatus: (
+    ((messageID: string, error: ?string) => mixed),
+  ) => RemoveCallback,
+};
diff --git a/lib/tunnelbroker/tunnelbroker-context.js b/lib/tunnelbroker/tunnelbroker-context.js
--- a/lib/tunnelbroker/tunnelbroker-context.js
+++ b/lib/tunnelbroker/tunnelbroker-context.js
@@ -4,6 +4,7 @@
 import * as React from 'react';
 import uuid from 'uuid';
 
+import type { SecondaryTunnelbrokerConnection } from './secondary-tunnelbroker-connection.js';
 import { tunnnelbrokerURL } from '../facts/tunnelbroker.js';
 import { peerToPeerMessageHandler } from '../handlers/peer-to-peer-message-handler.js';
 import { IdentityClientContext } from '../shared/identity-client-context.js';
@@ -58,6 +59,7 @@
   +shouldBeClosed?: boolean,
   +onClose?: () => mixed,
   +initMessage: ?ConnectionInitializationMessage,
+  +secondaryTunnelbrokerConnection?: SecondaryTunnelbrokerConnection,
 };
 
 function createAnonymousInitMessage(
@@ -76,6 +78,7 @@
     shouldBeClosed,
     onClose,
     initMessage: initMessageProp,
+    secondaryTunnelbrokerConnection,
   } = props;
   const [connected, setConnected] = React.useState(false);
   const listeners = React.useRef<Set<TunnelbrokerSocketListener>>(new Set());
@@ -269,12 +272,32 @@
     onClose,
   ]);
 
+  const sendMessageToDeviceRequest: (
+    request: MessageToDeviceRequest,
+  ) => Promise<void> = React.useCallback(
+    request => {
+      return new Promise((resolve, reject) => {
+        const socketActive = connected && socket.current;
+        if (!shouldBeClosed && !socketActive) {
+          throw new Error('Tunnelbroker not connected');
+        }
+        promises.current[request.clientMessageID] = {
+          resolve,
+          reject,
+        };
+        if (socketActive) {
+          socket.current?.send(JSON.stringify(request));
+        } else {
+          secondaryTunnelbrokerConnection?.sendMessage(request);
+        }
+      });
+    },
+    [connected, secondaryTunnelbrokerConnection, shouldBeClosed],
+  );
+
   const sendMessage: (message: ClientMessageToDevice) => Promise<void> =
     React.useCallback(
       (message: ClientMessageToDevice) => {
-        if (!connected || !socket.current) {
-          throw new Error('Tunnelbroker not connected');
-        }
         const clientMessageID = uuid.v4();
         const messageToDevice: MessageToDeviceRequest = {
           type: tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE_REQUEST,
@@ -283,17 +306,53 @@
           payload: message.payload,
         };
 
-        return new Promise((resolve, reject) => {
-          promises.current[clientMessageID] = {
-            resolve,
-            reject,
-          };
-          socket.current?.send(JSON.stringify(messageToDevice));
-        });
+        return sendMessageToDeviceRequest(messageToDevice);
       },
-      [connected],
+      [sendMessageToDeviceRequest],
     );
 
+  React.useEffect(
+    () =>
+      secondaryTunnelbrokerConnection?.onSendMessage(message => {
+        if (shouldBeClosed) {
+          // We aren't supposed to be handling it
+          return;
+        }
+
+        void (async () => {
+          try {
+            await sendMessageToDeviceRequest(message);
+            secondaryTunnelbrokerConnection.setMessageStatus(
+              message.clientMessageID,
+            );
+          } catch (error) {
+            secondaryTunnelbrokerConnection.setMessageStatus(
+              message.clientMessageID,
+              error,
+            );
+          }
+        })();
+      }),
+    [
+      secondaryTunnelbrokerConnection,
+      sendMessageToDeviceRequest,
+      shouldBeClosed,
+    ],
+  );
+
+  React.useEffect(
+    () =>
+      secondaryTunnelbrokerConnection?.onMessageStatus((messageID, error) => {
+        if (error) {
+          promises.current[messageID].reject(error);
+        } else {
+          promises.current[messageID].resolve();
+        }
+        delete promises.current[messageID];
+      }),
+    [secondaryTunnelbrokerConnection],
+  );
+
   const addListener = React.useCallback(
     (listener: TunnelbrokerSocketListener) => {
       listeners.current.add(listener);
diff --git a/web/app.react.js b/web/app.react.js
--- a/web/app.react.js
+++ b/web/app.react.js
@@ -28,10 +28,12 @@
 } from 'lib/selectors/loading-selectors.js';
 import { isLoggedIn } from 'lib/selectors/user-selectors.js';
 import { extractMajorDesktopVersion } from 'lib/shared/version-utils.js';
+import type { SecondaryTunnelbrokerConnection } from 'lib/tunnelbroker/secondary-tunnelbroker-connection.js';
 import { TunnelbrokerProvider } from 'lib/tunnelbroker/tunnelbroker-context.js';
 import type { LoadingStatus } from 'lib/types/loading-types.js';
 import type { WebNavInfo } from 'lib/types/nav-types.js';
 import type { Dispatch } from 'lib/types/redux-types.js';
+import type { MessageToDeviceRequest } from 'lib/types/tunnelbroker/message-to-device-request-types.js';
 import { getConfig, registerConfig } from 'lib/utils/config.js';
 import { useDispatch } from 'lib/utils/redux-utils.js';
 import { infoFromURL } from 'lib/utils/url-utils.js';
@@ -361,6 +363,119 @@
   }
 }
 
+const WEB_TUNNELBROKER_CHANNEL = new BroadcastChannel('shared-tunnelbroker');
+const WEB_TUNNELBROKER_MESSAGE_TYPES = Object.freeze({
+  SEND_MESSAGE: 'send-message',
+  MESSAGE_STATUS: 'message-status',
+});
+
+function useOtherTabsTunnelbrokerConnection(): SecondaryTunnelbrokerConnection {
+  const onSendMessageCallbacks = React.useRef<
+    Set<(MessageToDeviceRequest) => mixed>,
+  >(new Set());
+
+  const onMessageStatusCallbacks = React.useRef<
+    Set<(messageID: string, error: ?string) => mixed>,
+  >(new Set());
+
+  React.useEffect(() => {
+    const messageHandler = (event: MessageEvent) => {
+      if (typeof event.data !== 'object' || !event.data) {
+        console.log(
+          'Invalid message received from shared ' +
+            'tunnelbroker broadcast channel',
+          event.data,
+        );
+        return;
+      }
+      const data = event.data;
+      if (data.type === WEB_TUNNELBROKER_MESSAGE_TYPES.SEND_MESSAGE) {
+        if (typeof data.message !== 'object' || !data.message) {
+          console.log(
+            'Invalid tunnelbroker message request received ' +
+              'from shared tunnelbroker broadcast channel',
+            event.data,
+          );
+          return;
+        }
+        // We know that the input was already validated
+        const message: MessageToDeviceRequest = (data.message: any);
+
+        for (const callback of onSendMessageCallbacks.current) {
+          callback(message);
+        }
+      } else if (data.type === WEB_TUNNELBROKER_MESSAGE_TYPES.MESSAGE_STATUS) {
+        if (typeof data.messageID !== 'string') {
+          console.log(
+            'Missing message id in message status message ' +
+              'from shared tunnelbroker broadcast channel',
+          );
+          return;
+        }
+        const messageID = data.messageID;
+
+        if (
+          typeof data.error !== 'string' &&
+          data.error !== null &&
+          data.error !== undefined
+        ) {
+          console.log(
+            'Invalid error in message status message ' +
+              'from shared tunnelbroker broadcast channel',
+            data.error,
+          );
+          return;
+        }
+        const error = data.error;
+
+        for (const callback of onMessageStatusCallbacks.current) {
+          callback(messageID, error);
+        }
+      } else {
+        console.log(
+          'Invalid message type ' +
+            'from shared tunnelbroker broadcast channel',
+          data,
+        );
+      }
+    };
+
+    WEB_TUNNELBROKER_CHANNEL.addEventListener('message', messageHandler);
+    return () =>
+      WEB_TUNNELBROKER_CHANNEL.removeEventListener('message', messageHandler);
+  }, [onMessageStatusCallbacks, onSendMessageCallbacks]);
+
+  return React.useMemo(
+    () => ({
+      sendMessage: message =>
+        WEB_TUNNELBROKER_CHANNEL.postMessage({
+          type: WEB_TUNNELBROKER_MESSAGE_TYPES.SEND_MESSAGE,
+          message,
+        }),
+      onSendMessage: callback => {
+        onSendMessageCallbacks.current.add(callback);
+        return () => {
+          onSendMessageCallbacks.current.delete(callback);
+        };
+      },
+      setMessageStatus: (messageID, error) => {
+        WEB_TUNNELBROKER_CHANNEL.postMessage({
+          type: WEB_TUNNELBROKER_MESSAGE_TYPES.MESSAGE_STATUS,
+          messageID,
+          error,
+        });
+      },
+      onMessageStatus: callback => {
+        onMessageStatusCallbacks.current.add(callback);
+        return () => {
+          onMessageStatusCallbacks.current.delete(callback);
+        };
+      },
+    }),
+    [onMessageStatusCallbacks, onSendMessageCallbacks],
+  );
+}
+
 const fetchEntriesLoadingStatusSelector = createLoadingStatusSelector(
   fetchEntriesActionTypes,
 );
@@ -411,12 +526,16 @@
       TUNNELBROKER_LOCK_NAME,
     );
 
+    const secondaryTunnelbrokerConnection: SecondaryTunnelbrokerConnection =
+      useOtherTabsTunnelbrokerConnection();
+
     return (
       <AppThemeWrapper>
         <TunnelbrokerProvider
           initMessage={tunnelbrokerInitMessage}
           shouldBeClosed={lockStatus !== 'acquired'}
           onClose={releaseLockOrAbortRequest}
+          secondaryTunnelbrokerConnection={secondaryTunnelbrokerConnection}
         >
           <IdentitySearchProvider>
             <App