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 sessionconst 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
Property | crypto.randomUUID() |
---|---|
Type | UUID v4 |
Uniqueness Guarantee | Statistical (not absolute) |
Random Bits | 122 |
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?
- 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.
- 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});
- 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:
Scenario | Effect on WebSocket connection |
---|---|
Multi-page navigation | Socket disconnects and reconnects |
SPA (client-side routing) | Socket stays connected |
Manual reconnect on load | New 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?
- 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).
- 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:
// Clientconst sessionId = localStorage.getItem('session_id') || generateNewSessionId();localStorage.setItem('session_id', sessionId);
const socket = io({ auth: { sessionId } });
socket.on('connect', () => { socket.emit('restore_state', { sessionId });});
// Serverio.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 connectsonconnect = (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 SharedWorkerfunction 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); });});