Real-time communication in ES5 environments

Understand how WebSocket and Socket.IO work and which option suits old-school JavaScript.

Why WebSockets?

The native WebSocket interface opens a full-duplex channel between browser and server, eliminating HTTP polling latency. It is stable and widely supported, including IE10+ and any ES5-capable browser.

Why Socket.IO?

Socket.IO wraps WebSockets with automatic fall-backs (long-polling, JSONP) and higher-level features such as rooms, namespaces, and automatic reconnection. Official docs confirm support down to IE9, but later client builds require ES2015 syntax, so pinning to socket.io-client ~2.2.0 keeps pure ES5 output.

Compatibility at a glance

AspectWebSocket API + wsSocket.IO v2.2
Browser runtimeNative ES5, IE10+Bundled ES5 script, IE9+
FallbacksNoneXHR-long-polling if WS blocked
Server installnpm i wsnpm i socket.io
Extra featuresPing/pong, per-message deflateRooms, namespaces, acknowledgments
Bundle size (gzip)0 KB (browser native)≈15 KB

Building a WebSocket server with ws

Lightweight, fast, zero dependencies – perfect for Node v0.10 → v22.

Installation

Run npm install ws. The package is a pure-JS implementation that passes the full Autobahn test-suite and supports per-message-deflate compression by default.

Minimal echo server


// server.js (ES5)
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function (ws) {
  ws.on('message', function (msg) { ws.send(msg); });
  ws.send('Hello client!');
});

Broadcast helper


function broadcast(data) {
  wss.clients.forEach(function each(client) {
    if (client.readyState === client.OPEN) { client.send(data); }
  });
}

Heartbeat (ping / pong)

Call ws.isAlive = true on each pong, then iterate wss.clients every 30 s and terminate dead sockets. This prevents phantom connections in flaky networks.

Securing the channel


// wss.js — HTTPS upgrade
var https = require('https');
var fs    = require('fs');
var WebSocketServer = require('ws').Server;

var server = https.createServer({
  cert: fs.readFileSync('./cert.pem'),
  key : fs.readFileSync('./key.pem')
}).listen(8443);

var wss = new WebSocketServer({ server: server });

Scaling with PM2

Use pm2 start server.js -i max --watch for cluster mode. PM2 balances TCP connections across workers. Sticky-sessions are unnecessary because WebSocket hand-off happens after the HTTP upgrade completes on each worker.

Browser client in plain ES5

Works on any browser shipping the WebSocket interface (IE10+, BB10 2015).

Connection & messaging


// client.js — attach after DOM ready
var ws = new WebSocket('ws://'+location.host+':8080');

ws.onopen    = function ()  { log('open'); ws.send('Hi server'); };
ws.onmessage = function (ev) { log('got: '+ ev.data);           };
ws.onclose   = function ()  { log('closed');                    };

function log(txt){ document.getElementById('out').innerHTML += txt + '
'; }

Automatic reconnection pattern


function Reconnect(url, delay){
  var sock; var timer;
  function connect(){
     sock = new WebSocket(url);
     sock.onopen    = function(){ clearTimeout(timer); };
     sock.onclose   = function(){ timer = setTimeout(connect, delay); };
     sock.onmessage = function(e){ /* … */ };
  }
  connect();
}
Reconnect('ws://'+location.host+':8080', 2000);

JSON envelope convention

Send strings like { "type":"chat","payload":"hello" } to allow feature-switching without breaking older clients.

Using Socket.IO in legacy projects

Feature-rich solution for ES5 when pinned to v2.2.x.

Keeping ES5 output

Versions ≥2.3 include a debug 4.x dependency which introduces const; pin to ~2.2.0 in package.json or transpile.

Server bootstrap


// io-server.js
var http = require('http').createServer();
var io   = require('socket.io')(http);

io.on('connection', function (socket) {
  socket.emit('hello', 'welcome');
  socket.on('chat', function (msg) { io.emit('chat', msg); });
});
http.listen(8080);

Browser script include




Rooms & namespaces

Call socket.join('room1') to group clients, then broadcast with io.to('room1').emit('event', data). Namespaces are created by prefixing the path: io.of('/admin').

Reconnection logic

The client automatically retries up to 5 times with exponential back-off; configure through reconnectionAttempts, reconnectionDelay.

Hardening & production check-list

Protect the channel and keep your server healthy.

Transport security

Always serve over TLS (wss) in production. Use LetsEncrypt or place Node behind an Nginx reverse proxy terminating SSL.

Origin & CORS

Validate the Origin header in WebSocket upgrade requests. For ws, check req.headers.origin; for Socket.IO, set cors policy on the server constructor.

Authentication

Pass a signed JWT as a URL query (?token=…) or in the initial HTTP cookies, then verify in your connection handler before accepting the socket.

Rate limiting

Track message counts per socket / IP. Disconnect aggressive peers and throttle broadcast loops.

Resource cleanup

On every close, clear timers and erase socket references to prevent memory leaks.

Debugging workflow

Trace packets and reproduce edge-cases.

Wire logs

For ws, attach ws.on('error', console.error) and run Node with NODE_DEBUG=net,tls. For Socket.IO, set localStorage.debug = 'socket.io-client:*' in the browser console.

Network simulators

Use Chrome DevTools → Network conditions or tc on Linux to inject latency and packet loss, confirming your reconnection code works.

Replay scripts

Write Node tests with ws client instances that send prerecorded JSON frames to ensure backward compatibility whenever you change message formats.