Diagnosing Socket.IO WebSocket Upgrade Failures and Long-Polling Fallback
Clients stuck on Socket.IO long-polling? Confirm the transport, isolate proxy or server causes, and apply the matching fix.
26 May 2026, 03:07 UTC

Recognizable condition
A Socket.IO client completes the opening handshake, but the browser Network tab continues to show repeated requests to /socket.io/?EIO=...&transport=polling. You may see elevated latency, many small POST and GET pairs, and occasional upgrade warnings. The connection works, but it never settles onto a single WebSocket socket.
Long-polling is Socket.IO’s normal fallback when a network blocks non-HTTP traffic. It is not a fault by itself unless it violates latency or disconnect SLAs.
Cause and diagnostic table
| Observation | Likely cause | Check first |
|---|---|---|
| Repeated polling requests; no WebSocket frames | Reverse proxy is not forwarding Upgrade/Connection headers | Proxy configuration |
| Upgrade request blocked or rejected | CORS origin does not match the client URL | CORS configuration |
| Handshake rejected or silent fallback | Socket.IO major versions differ between server and client | Installed package versions |
| Works on one node, fails on another | Missing sticky sessions or Redis adapter misconfiguration | Load balancer and adapter state |
Ordered checks
- Confirm the transport in the browser. Open DevTools, go to the Network tab, and filter for
socket.io. A healthy connection starts with one or two polling requests, then switches totransport=websocket. If every request carriestransport=polling, the upgrade did not happen. - Check server logs. Look for the transport the server selected during handshake. If the server logs polling while the client expects WebSocket, server configuration or a middleware layer is forcing the fallback.
- Inspect the reverse proxy. Socket.IO upgrades require the proxy to pass the
UpgradeandConnectionheaders. Missing headers cause the proxy to treat the upgrade as a regular HTTP request and keep the client on polling. - Compare Socket.IO major versions. Check
package.jsonor the lockfile on the server and client. Socket.IO major versions use different Engine.IO protocols; mismatched pairs can reject the handshake or fall back silently. - Bypass the proxy. Run the server locally or connect directly to its public port from the same client. If WebSocket works directly but not through the proxy, the issue is infrastructure, not application code.
Fixes tied to findings
Reverse proxy missing upgrade headers
Edit the proxy location block for your Socket.IO path. Example for Nginx:
location /socket.io/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
Run nginx -t on the proxy host to validate syntax. You need root or sudo privileges to edit the config and reload the service. Reload with sudo systemctl reload nginx. Risk: a malformed config prevents reload and can leave the old configuration running or fail to start after a restart.
Rollback: restore the previous proxy configuration file, run nginx -t, and reload again.
CORS origin mismatch
Set the Socket.IO CORS origin to the exact client URL. Example Node.js server setup:
const { Server } = require("socket.io");
const io = new Server(server, {
transports: ["websocket", "polling"],
cors: {
origin: "https://app.example.com",
methods: ["GET", "POST"]
}
});
On the client, use the same origin and transport preference:
const socket = io("https://app.example.com", {
transports: ["websocket", "polling"]
});
Avoid wildcard origins (*) if the client sends cookies or authorization headers, because browsers restrict credentials with wildcard CORS.
Version mismatch
Run npm ls socket.io in the server and client project directories. The command shows the installed major version. Both sides should belong to the same Socket.IO major version family. Upgrade the older side to match, then restart both processes.
Sticky sessions or adapter state
If you run multiple server nodes behind a load balancer, enable sticky sessions for the Socket.IO namespace, or use the Redis adapter so rooms and broadcast state are shared across nodes. Without affinity or shared state, a client can connect to node A and send its next polling request to node B, which does not recognize the session.
Escalation criteria
- Direct server access still fails WebSocket upgrade. Escalate to application or platform engineering. Suspect server code, middleware, or Socket.IO/Engine.IO version mismatch.
- Direct access works, but the proxy fails. Escalate to infrastructure with evidence: DevTools transport column, proxy configuration, and packet capture showing whether the Upgrade header reaches the backend.
- Failures are intermittent across nodes. Suspect load balancer affinity or Redis adapter misconfiguration. Collect logs from each node during a failure window.
Limitations and practical verification
Do not treat long-polling as a bug if users are on restricted corporate networks. Restricting transports to ["websocket"] excludes those clients entirely. Keep polling as a fallback unless you control every client network.
Practical verification: after applying a fix, reload the client and confirm the Network tab shows transport=websocket after the initial polling handshake. Repeat the direct-connection test if the proxy was involved. Confirm version alignment with npm ls socket.io on both sides.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.