Skip to content

Fix: Socket.IO Not Connecting (CORS, Transport, and Namespace Errors)

FixDevs ·

Quick Answer

How to fix Socket.IO connection failures — CORS errors, transport fallback issues, wrong namespace, server not emitting to the right room, and how to debug Socket.IO connections in production.

The Error

The Socket.IO client fails to connect and logs errors like:

GET http://localhost:3001/socket.io/?EIO=4&transport=polling 404 (Not Found)

Or a CORS error:

Access to XMLHttpRequest at 'http://localhost:3001/socket.io/?...' from origin
'http://localhost:3000' has been blocked by CORS policy

Or the connection times out silently — the connect event never fires and the client keeps retrying:

socket.on('connect_error', (err) => {
  console.log(err.message); // "xhr poll error" or "websocket error"
});

Or the client connects but events are never received even though the server is emitting them.

Why This Happens

Socket.IO connections fail for several distinct reasons:

  • CORS misconfiguration — Socket.IO requires explicit CORS configuration on the server. The default is to allow all origins in development, but in production it must be configured.
  • Wrong server URL or port — the client connects to a different port or path than where the Socket.IO server is listening.
  • Transport mismatch — by default Socket.IO starts with HTTP long-polling and upgrades to WebSocket. If the server or a proxy blocks WebSocket upgrades, the connection hangs.
  • Wrong namespace — connecting to /admin when the server defines /dashboard means the client connects but receives no events.
  • Proxy not configured for WebSocket — nginx, a load balancer, or a CDN terminates WebSocket connections without forwarding them.
  • Version mismatch — Socket.IO v4 client is not compatible with a Socket.IO v2 or v3 server.

Fix 1: Configure CORS on the Server

// server.js — Express + Socket.IO
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');

const app = express();
const httpServer = createServer(app);

const io = new Server(httpServer, {
  cors: {
    origin: 'http://localhost:3000',      // Client origin — must match exactly
    methods: ['GET', 'POST'],
    credentials: true,                    // Required if client sends cookies
  },
});

// For multiple origins
const io = new Server(httpServer, {
  cors: {
    origin: [
      'http://localhost:3000',
      'https://myapp.com',
      'https://www.myapp.com',
    ],
    methods: ['GET', 'POST'],
  },
});

// For development — allow all origins (not for production)
const io = new Server(httpServer, {
  cors: { origin: '*' },
});

io.on('connection', (socket) => {
  console.log('Client connected:', socket.id);
});

httpServer.listen(3001);

Client — match the server URL:

import { io } from 'socket.io-client';

const socket = io('http://localhost:3001', {  // Server port, not client port
  withCredentials: true,   // Must match server's credentials: true
});

Common Mistake: Running the Express API on port 3001 and the React dev server on port 3000. The Socket.IO client must connect to port 3001 (where Socket.IO lives), not port 3000. Set the client URL to http://localhost:3001.

Fix 2: Fix Transport Issues (WebSocket Upgrade Fails)

If connections hang on long-polling and never upgrade to WebSocket, force WebSocket-only or fix the proxy:

Client — force WebSocket transport:

const socket = io('http://localhost:3001', {
  transports: ['websocket'],  // Skip polling, use WebSocket directly
});

Client — fallback order (default behavior):

const socket = io('http://localhost:3001', {
  transports: ['polling', 'websocket'],  // Start with polling, upgrade to WS
});

Fix nginx for WebSocket proxying:

# nginx.conf — proxy both HTTP polling and WebSocket upgrade
location /socket.io/ {
    proxy_pass http://localhost:3001;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;       # Required for WebSocket
    proxy_set_header Connection "upgrade";         # Required for WebSocket
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_cache_bypass $http_upgrade;
    proxy_read_timeout 86400;                      # Keep alive for long-polling
}

Fix when deployed behind a load balancer (sticky sessions):

HTTP long-polling requires multiple requests to hit the same server instance. Without sticky sessions, requests land on different servers that don’t share socket state:

// server.js — use Redis adapter for multi-server deployments
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');

const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();

await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));

Or force WebSocket-only on the client (WebSocket is stateful — no sticky session needed):

const socket = io({ transports: ['websocket'] });

Fix 3: Fix Namespace Mismatch

Socket.IO namespaces are like separate channels. If the client and server use different namespaces, they cannot communicate:

Server — defining a namespace:

// Default namespace is '/'
io.on('connection', (socket) => { /* all clients on / */ });

// Custom namespace
const adminNsp = io.of('/admin');
adminNsp.on('connection', (socket) => { /* only /admin clients */ });

const dashboardNsp = io.of('/dashboard');
dashboardNsp.on('connection', (socket) => { /* only /dashboard clients */ });

Client — connecting to a namespace:

// Default namespace
const socket = io('http://localhost:3001');
// Equivalent to: io('http://localhost:3001/')

// Custom namespace — must match server exactly
const socket = io('http://localhost:3001/admin');
const socket = io('http://localhost:3001/dashboard');

// Wrong — this connects to '/' not '/admin'
const socket = io('http://localhost:3001', { path: '/admin' });
// 'path' is for the Socket.IO server path, not the namespace

Note: The path option (default: /socket.io) is different from a namespace. The path is the HTTP endpoint for the Socket.IO handshake. The namespace is a logical channel within a single Socket.IO server.

Fix 4: Fix Room and Event Emission Issues

If the client connects successfully but never receives events:

Verify the client is in the right room:

// Server — emit to a room
io.to('room-123').emit('message', data);

// Client must have joined the room first
socket.on('connect', () => {
  socket.emit('join-room', 'room-123');
});

// Server — handle room join
io.on('connection', (socket) => {
  socket.on('join-room', (roomId) => {
    socket.join(roomId);
    console.log(`${socket.id} joined room ${roomId}`);
  });
});

Common emission mistakes:

// Server-side emission patterns

// Wrong — emits only to the sender
socket.emit('event', data);

// Correct — emits to all clients
io.emit('event', data);

// Correct — emits to all clients in a room
io.to('room-id').emit('event', data);

// Correct — emits to all clients except the sender
socket.broadcast.emit('event', data);

// Correct — emits to a specific socket by ID
io.to(socketId).emit('event', data);

Verify event names match exactly:

// Server emits 'user-joined'
socket.emit('user-joined', { name: 'Alice' });

// Client listens for 'userJoined' — event name mismatch, never received
socket.on('userJoined', (data) => { ... }); // ← Wrong

// Fix — exact same event name
socket.on('user-joined', (data) => { ... }); // ✓

Fix 5: Fix Socket.IO Behind a Reverse Proxy (Path Configuration)

When Socket.IO is mounted at a sub-path (not the root):

Server:

const io = new Server(httpServer, {
  path: '/ws/socket.io',  // Custom path instead of default /socket.io
  cors: { origin: 'https://myapp.com' },
});

Client:

const socket = io('https://myapp.com', {
  path: '/ws/socket.io',  // Must match server path
});

nginx for sub-path:

location /ws/ {
    proxy_pass http://localhost:3001;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
}

Fix 6: Fix Version Compatibility

Socket.IO v4 (client) is not compatible with Socket.IO v2/v3 (server):

# Check installed versions
cat package.json | grep socket.io
npm list socket.io socket.io-client

# Upgrade both server and client together
npm install socket.io@latest           # Server
npm install socket.io-client@latest    # Client

Major version compatibility:

ServerClient
v4.xv4.x (compatible)
v3.xv3.x (compatible)
v4.xv3.x
// Allow older clients to connect to v4 server
const io = new Server(httpServer, {
  allowEIO3: true,  // Accept Socket.IO v2/v3 clients
});

Fix 7: Debug Socket.IO Connections

Enable debug logging:

// Browser console — enable Socket.IO debug logs
localStorage.debug = 'socket.io-client:*';

// Node.js server
DEBUG=socket.io* node server.js

Listen to all connection events on the client:

const socket = io('http://localhost:3001');

socket.on('connect', () => {
  console.log('Connected! Socket ID:', socket.id);
  console.log('Transport:', socket.io.engine.transport.name); // 'polling' or 'websocket'
});

socket.on('connect_error', (error) => {
  console.error('Connection error:', error.message, error.description);
});

socket.on('disconnect', (reason) => {
  console.log('Disconnected:', reason);
  // reason: 'io server disconnect' | 'io client disconnect' | 'ping timeout' | 'transport close' | 'transport error'
});

socket.io.on('reconnect_attempt', (attempt) => {
  console.log('Reconnect attempt:', attempt);
});

Check the Socket.IO engine status:

socket.on('connect', () => {
  const transport = socket.io.engine.transport.name;
  console.log('Initial transport:', transport); // 'polling'

  socket.io.engine.on('upgrade', () => {
    const upgraded = socket.io.engine.transport.name;
    console.log('Upgraded to:', upgraded); // 'websocket'
  });
});

Still Not Working?

Test WebSocket directly. Use wscat to check if WebSocket connections work on the server:

npm install -g wscat
wscat -c ws://localhost:3001/socket.io/?EIO=4&transport=websocket

Check if the Socket.IO server is actually running on the expected port:

lsof -i :3001
netstat -tulnp | grep 3001

Check for double-initialization. In Express, attaching Socket.IO to app instead of the HTTP server is a common mistake:

// Wrong — attaches to Express app, not HTTP server
const io = new Server(app);

// Correct — attaches to the HTTP server
const httpServer = createServer(app);
const io = new Server(httpServer);
httpServer.listen(3001);

For related real-time and WebSocket issues, see Fix: nginx WebSocket Proxy Not Working and Fix: CORS Access-Control-Allow-Origin Error.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles