Skip to content

4. Multiple Browser Tabs

When a user opens your app in another browser tab, it creates a new WebSocket connection, resulting in a separate socket instance on the server.

⚠️ Why this happens

Each tab is a new browser context, with:

  • A separate WebSocket handshake
  • A unique socket ID

✅ Goal

You want to detect or prevent multiple tabs per client, or treat them as a single session.

1. Opt 01: Use a Shared session ID

Use a Shared session ID (via cookies or localStorage)

  • Generate a user/session ID once.
  • Store it in localStorage or a cookie.
  • On each connection, the client sends this ID to the server.
  • Server can track or reject duplicate connections.

1.1 🧪 Example:

Client-side (index.html)

const sessionId = localStorage.getItem('session_id') || crypto.randomUUID();
localStorage.setItem('session_id', sessionId);
const socket = io({
auth: {
sessionId: sessionId
}
});

Server-side (server.js)

const activeSessions = new Map(); // sessionId → socket
io.use((socket, next) => {
const sessionId = socket.handshake.auth.sessionId;
if (!sessionId) {
return next(new Error('No session ID'));
}
// Reject if this session is already connected
if (activeSessions.has(sessionId)) {
return next(new Error('Session already active'));
}
socket.sessionId = sessionId;
next();
});
io.on('connection', (socket) => {
console.log('Connected:', socket.sessionId);
activeSessions.set(socket.sessionId, socket);
socket.on('disconnect', () => {
console.log('Disconnected:', socket.sessionId);
activeSessions.delete(socket.sessionId);
});
});

👀 What This Does

  • localStorage is shared across tabs in the same browser, so sessionId remains the same.
  • Server blocks duplicate sessions from connecting.
  • If a tab is closed, the socket disconnects and the session becomes available again.

2. Opt 02: Multiple Sockets Per User

Allow multiple sockets per user.

// Use a Set to store multiple sockets per session
const userSockets = new Map();
io.on('connection', (socket) => {
const sessionId = socket.handshake.auth.sessionId;
if (!userSockets.has(sessionId)) userSockets.set(sessionId, new Set());
userSockets.get(sessionId).add(socket);
socket.on('disconnect', () => {
userSockets.get(sessionId).delete(socket);
if (userSockets.get(sessionId).size === 0) {
userSockets.delete(sessionId);
}
});
});

This way you support multiple tabs, but treat them logically as one user.

3. Uniquesness with UUID

No, crypto.randomUUID() does not guarantee absolute global uniqueness, but it generates a UUID v4 which is statistically unique with extremely high probability.

🔍 What is crypto.randomUUID()?

  • Part of the Web Crypto API
  • Generates a UUID v4 (random)
  • Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
  • Uses 122 bits of randomness

✅ Probability of Collision

  • There are 2¹²² possible UUID v4 values.
  • You’d have to generate billions of UUIDs per second for thousands of years to have a statistically significant chance of a collision.

📈 For reference:

The chance of generating a duplicate UUID v4 in 1 billion UUIDs is roughly 1 in 1 quintillion (10¹⁸).

🔐 Is It Safe to Use Globally?

Yes, in practice:

  • crypto.randomUUID() is suitable for session IDs, user IDs, client tracking, etc.
  • Used widely in production systems without collisions

But:

  • It’s not cryptographically verifiable (unlike UUID v5 or signed JWTs).
  • It’s not guaranteed by specification — just statistically safe.

✅ Use Cases Where It’s Safe

  • Socket session tracking
  • Temporary user identifiers
  • Client-side IDs

❌ When Not to Use It

If you need:

  • Deterministic uniqueness across systems
  • Signed or verifiable identity (e.g., for secure auth tokens)

Use something like:

  • UUID v5 (namespace + input → deterministic)
  • JWTs
  • Server-generated IDs with checks (e.g. database UUIDs with uniqueness constraint)

🧪 Summary

Propertycrypto.randomUUID()
TypeUUID v4
Uniqueness GuaranteeStatistical (not absolute)
Random Bits122
Safe for Global Use?✅ Yes, for nearly all practical uses

UUID v5 (deterministic based on input), or a hybrid approach can be used for extra safety.

4. Navigating From Pages

When you navigate from one page to another in a traditional multi-page web app (MPA), the browser unloads the current page, which closes the existing WebSocket connection. So the socket disconnects.

Why?

  • WebSocket connection lives as long as the page (tab) is open and active.
  • Navigating to a new page reloads the whole page, tearing down the previous socket.
  • The new page will create a new WebSocket connection (a new socket).

How to handle this?

  1. Single Page Application (SPA)
  • If you use frameworks like React, Vue, Angular, the app stays loaded.
  • Navigation is client-side, so WebSocket stays connected.
  • Great for maintaining persistent socket connections.
  1. Reconnect on page load
  • On every page load, your client script should create a new socket connection.
  • You can handle reconnection logic automatically with Socket.IO’s built-in support.
const socket = io({
reconnectionAttempts: 5,
reconnectionDelay: 1000
});
  1. Use SharedWorker or Service Worker (advanced)
  • You can create a shared WebSocket connection at the browser level using SharedWorker.
  • All tabs/pages can communicate with the worker and share the socket.
  • This is more complex and not widely used yet.

Summary:

ScenarioEffect on WebSocket connection
Multi-page navigationSocket disconnects and reconnects
SPA (client-side routing)Socket stays connected
Manual reconnect on loadNew socket created after page load

On page load, it will be a new socket connection, with a new socket ID from the server.

What happens during page navigation?

  • The old WebSocket connection closes (because the page unloads).
  • The new page creates a new WebSocket connection.
  • The server treats this as a completely new socket.

Implications of new socket on page reload/navigation

  • The server sees it as a new client connection.
  • Any socket-specific state on the server tied to the old socket ID is lost.
  • You need to re-establish any session or user context on the new socket.

How to handle this properly?

  1. Use a persistent session ID or token
  • Pass a session token or user ID when connecting.
  • On connection, server restores context based on that token (e.g., user data, permissions).
  1. Re-sync client and server state
  • After connection, client emits an event like restore_state with info about what it needs.
  • Server responds with current state/data so client can continue smoothly.

Quick example of passing session ID on connection:

// Client
const sessionId = localStorage.getItem('session_id') || generateNewSessionId();
localStorage.setItem('session_id', sessionId);
const socket = io({ auth: { sessionId } });
socket.on('connect', () => {
socket.emit('restore_state', { sessionId });
});
// Server
io.use((socket, next) => {
const sessionId = socket.handshake.auth.sessionId;
if (!sessionId) return next(new Error("No session ID"));
socket.sessionId = sessionId;
next();
});
io.on('connection', (socket) => {
console.log("New connection for session:", socket.sessionId);
socket.on('restore_state', (data) => {
// Retrieve state for data.sessionId and send back
socket.emit('state_data', {/*...*/});
});
});

Summary

  • New page load = new socket connection with a new socket ID.
  • To maintain continuity, use a session ID or token shared between tabs/page loads.
  • Restore or sync app state after reconnecting.

5. Shared Worker

Here’s a concise explanation plus example on how to use a SharedWorker to maintain a single WebSocket connection shared across multiple tabs of the same origin.

Why use a SharedWorker?

  • WebSocket connection lives in the worker, independent of any page.
  • Multiple tabs connect to the same SharedWorker.
  • The worker manages one WebSocket and broadcasts messages to all connected tabs.
  • Avoids multiple WebSocket connections per user.

Basic architecture

[Tab 1] \
[Tab 2] -- connect --> [SharedWorker] <---> [Single WebSocket connection]
[Tab 3] /

Example: SharedWorker managing a single WebSocket

1. shared-worker.js
let connections = [];
let socket;
function setupSocket() {
socket = new WebSocket('wss://yourserver.example/ws');
socket.onmessage = (event) => {
// Broadcast to all connected tabs
connections.forEach(port => port.postMessage({ type: 'message', data: event.data }));
};
socket.onopen = () => {
connections.forEach(port => port.postMessage({ type: 'open' }));
};
socket.onclose = () => {
connections.forEach(port => port.postMessage({ type: 'close' }));
};
}
// Initialize WebSocket connection once the first tab connects
onconnect = (e) => {
const port = e.ports[0];
connections.push(port);
if (!socket || socket.readyState === WebSocket.CLOSED) {
setupSocket();
}
port.onmessage = (event) => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(event.data);
}
};
port.start();
port.postMessage({ type: 'connected' });
port.onclose = () => {
connections = connections.filter(p => p !== port);
if (connections.length === 0 && socket) {
socket.close();
}
};
};
2. Client page script
const worker = new SharedWorker('/shared-worker.js');
worker.port.start();
worker.port.onmessage = (event) => {
const { type, data } = event.data;
if (type === 'message') {
console.log('Received from server:', data);
} else if (type === 'open') {
console.log('WebSocket opened');
} else if (type === 'close') {
console.log('WebSocket closed');
}
};
// Send message to server through the SharedWorker
function sendMessage(msg) {
worker.port.postMessage(msg);
}

6. Tracking Socket with id

Each socket has a unique ID (socket.id). You could also store messages or socket references like:

const messageQueue = [];
const clientSockets = new Map(); // socket.id -> socket
io.on('connection', (socket) => {
clientSockets.set(socket.id, socket);
socket.on('chat message', (msg) => {
messageQueue.push({ msg, senderId: socket.id });
setTimeout(() => {
for (const [id, s] of clientSockets.entries()) {
if (id !== socket.id) {
s.emit('chat message', msg); // send to everyone except original sender
}
}
}, 5000);
});
socket.on('disconnect', () => {
clientSockets.delete(socket.id);
});
});