ws vs socket.io-client vs socketcluster-client vs engine.io-client vs faye-websocket vs primus vs sockjs-client
Real-Time Communication Libraries for Web Applications
wssocket.io-clientsocketcluster-clientengine.io-clientfaye-websocketprimussockjs-clientSimilar Packages:

Real-Time Communication Libraries for Web Applications

These libraries enable bidirectional, low-latency communication between clients and servers, essential for chat apps, live dashboards, and collaborative tools. They range from low-level WebSocket implementations to high-level abstractions with built-in reconnection, rooms, and fallback transports. Choosing the right one depends on your need for protocol standards, transport reliability, and server-side coupling.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
ws198,466,75622,742149 kB5a month agoMIT
socket.io-client10,978,89163,0581.42 MB2054 months agoMIT
socketcluster-client99,956300217 kB245 months agoMIT
engine.io-client063,058868 kB2054 months agoMIT
faye-websocket0613-55 years agoApache-2.0
primus04,472508 kB502 years agoMIT
sockjs-client08,521700 kB30-MIT

Real-Time Communication Libraries: Architecture, Protocols, and Trade-Offs

Building real-time features like chat, live notifications, or collaborative editing requires a solid communication layer. The JavaScript ecosystem offers several options, ranging from raw WebSocket implementations to high-level abstractions with built-in reliability features. This comparison breaks down seven key packages to help you choose the right tool for your architecture.

🏗️ Abstraction Level: Raw Sockets vs. Engineered Protocols

The most critical decision is whether you need raw WebSocket access or a managed protocol with reliability features.

ws provides a minimal, standards-compliant WebSocket implementation. It gives you full control but requires you to handle heartbeats, reconnection, and message framing manually.

// ws: Raw WebSocket connection
import WebSocket from 'ws';
const socket = new WebSocket('ws://localhost:8080');

socket.on('open', () => {
  socket.send(JSON.stringify({ type: 'init' }));
});

socket.on('message', (data) => {
  console.log('Received:', data.toString());
});

socket.io-client wraps the connection in a managed protocol. It handles reconnection, packet ordering, and disconnection detection automatically.

// socket.io-client: Managed connection
import { io } from 'socket.io-client';
const socket = io('http://localhost:8080');

socket.on('connect', () => {
  socket.emit('init', { userId: 123 });
});

socket.on('message', (data) => {
  console.log('Received:', data);
});

engine.io-client sits between these layers. It manages the transport (polling vs. websocket) but lacks high-level features like namespaces.

// engine.io-client: Transport layer only
import eio from 'engine.io-client';
const socket = new eio.Socket('ws://localhost:8080');

socket.on('open', () => {
  socket.send('init');
});

socket.on('message', (data) => {
  console.log('Received:', data);
});

sockjs-client mimics the WebSocket API but adds fallbacks. The API looks like ws, but the underlying transport can switch to HTTP if needed.

// sockjs-client: Emulated WebSocket
import SockJS from 'sockjs-client';
const socket = new SockJS('http://localhost:8080/stomp');

socket.onopen = () => {
  socket.send(JSON.stringify({ type: 'init' }));
};

socket.onmessage = (e) => {
  console.log('Received:', e.data);
};

primus abstracts the library entirely. You write code against the Primus API, and it compiles down to the underlying transformer (like ws or engine.io).

// primus: Library agnostic
import Primus from 'primus';
const socket = new Primus('http://localhost:8080');

socket.write({ type: 'init' });

socket.on('data', (data) => {
  console.log('Received:', data);
});

faye-websocket is a strict RFC-compliant client, often used for interoperability with specific servers like Faye.

// faye-websocket: RFC Compliant
import { Client } from 'faye-websocket';
const ws = new Client('ws://localhost:8080');

ws.on('open', () => {
  ws.send(JSON.stringify({ type: 'init' }));
});

ws.on('message', (event) => {
  console.log('Received:', event.data);
});

socketcluster-client is designed specifically for the SocketCluster server framework, focusing on pub/sub channels.

// socketcluster-client: Framework specific
import AGClient from 'socketcluster-client';
const socket = AGClient.create({ hostname: 'localhost', port: 8000 });

socket.subscribe('chat');

socket.watch('chat').subscribe((data) => {
  console.log('Received:', data);
});

🛡️ Reliability: Reconnection and Fallbacks

In production, networks are unstable. Some libraries handle this for you; others leave it to the developer.

socket.io-client has built-in exponential backoff reconnection. It also falls back to HTTP long-polling if WebSockets are blocked by firewalls or proxies.

// socket.io-client: Auto-reconnect config
const socket = io('http://localhost:8080', {
  reconnection: true,
  reconnectionAttempts: 5,
  reconnectionDelay: 1000,
  transports: ['websocket', 'polling'] // Fallback order
});

sockjs-client similarly handles fallbacks automatically, switching to XHR-streaming or other methods if the WebSocket handshake fails.

// sockjs-client: Automatic fallback
// No config needed; fallback is inherent to the protocol
const socket = new SockJS('http://localhost:8080/stomp');

ws does not reconnect automatically. You must implement your own logic to detect closure and re-establish the connection.

// ws: Manual reconnection
function connect() {
  const socket = new WebSocket('ws://localhost:8080');
  socket.onclose = () => setTimeout(connect, 1000);
  return socket;
}
const socket = connect();

primus includes reconnection logic similar to Socket.IO but configurable via plugins.

// primus: Reconnection plugin
const socket = new Primus('http://localhost:8080', {
  reconnect: {
    minDelay: 100,
    maxDelay: 10000,
    retries: 10
  }
});

engine.io-client handles transport-level reconnection but requires manual setup for application-level recovery.

// engine.io-client: Transport reconnect
const socket = new eio.Socket('ws://localhost:8080', {
  rememberUpgrade: true
});

faye-websocket requires manual reconnection handling, similar to ws.

// faye-websocket: Manual reconnect
function createConnection() {
  const ws = new Client('ws://localhost:8080');
  ws.on('close', () => setTimeout(createConnection, 1000));
  return ws;
}

socketcluster-client manages reconnection internally as part of the SocketCluster protocol.

// socketcluster-client: Built-in reconnect
const socket = AGClient.create({
  hostname: 'localhost',
  port: 8000,
  autoReconnect: true
});

📦 Server Coupling: Agnostic vs. Paired

Some clients are designed to work with any WebSocket server, while others require a specific matching server implementation.

ws is protocol-agnostic. It can connect to any standard WebSocket server (Node, Go, Python, etc.).

// ws: Connects to any RFC6455 server
const socket = new WebSocket('ws://any-server.com');

socket.io-client requires a Socket.IO server. It uses a custom packet format that generic WebSocket servers cannot parse.

// socket.io-client: Requires Socket.IO server
// Will NOT connect to a raw 'ws' server
const socket = io('http://socketio-server.com');

sockjs-client requires a SockJS server. The handshake protocol is specific to SockJS.

// sockjs-client: Requires SockJS server
const socket = new SockJS('http://sockjs-server.com/stomp');

primus is server-agnostic on the surface, but the server must also run Primus with a compatible transformer.

// primus: Requires Primus server
// Client and Server must agree on the transformer (e.g., 'ws')
const socket = new Primus('http://primus-server.com');

socketcluster-client requires a SocketCluster server. It leverages specific SC protocol features.

// socketcluster-client: Requires SocketCluster server
const socket = AGClient.create({ hostname: 'sc-server.com' });

engine.io-client requires an Engine.IO server (usually part of Socket.IO).

// engine.io-client: Requires Engine.IO server
const socket = new eio.Socket('http://engineio-server.com');

faye-websocket is generally agnostic but is often paired with Faye servers for pub/sub features.

// faye-websocket: Agnostic but often paired with Faye
const ws = new Client('ws://any-server.com');

🚀 Performance and Overhead

Lower-level libraries have less overhead but require more code. Higher-level libraries add bytes to your bundle but save development time.

ws has minimal overhead. It sends exactly what you tell it to send. Ideal for high-frequency trading or gaming where every millisecond counts.

// ws: Binary data support without framing overhead
socket.send(new Uint8Array([1, 2, 3]));

socket.io-client adds a small packet envelope for features like acknowledgments and rooms. This adds slight latency and bandwidth usage.

// socket.io-client: Acknowledgments add round-trips
socket.emit('event', data, (response) => {
  // Callback adds protocol overhead
});

sockjs-client adds overhead due to fallback mechanisms and heartbeat frames required to keep HTTP connections alive.

// sockjs-client: Heartbeat frames
// Internal framing adds bytes to every message
socket.send('message');

primus overhead depends on the chosen transformer. Using ws as a transformer minimizes this.

// primus: Overhead depends on transformer
// Minimal if configured with 'ws'
socket.write({ payload: 'data' });

engine.io-client is lighter than Socket.IO but heavier than raw ws due to transport switching logic.

// engine.io-client: Transport framing
socket.send('packet');

faye-websocket is lightweight but lacks optimization features found in newer libraries.

// faye-websocket: Standard framing
ws.send('data');

socketcluster-client is optimized for the SocketCluster pub/sub model, which can be more efficient for fan-out scenarios than room-based models.

// socketcluster-client: PubSub optimized
socket.transmitPublish('channel', data);

🛑 Deprecation and Maintenance Status

Before adopting a library, verify its long-term viability.

faye-websocket is in maintenance mode. It is stable but rarely updated. For new projects, prefer ws or socket.io-client.

// faye-websocket: Legacy status
// Use only if maintaining legacy Faye infrastructure
import { Client } from 'faye-websocket';

engine.io-client is actively maintained as part of the Socket.IO ecosystem but is rarely used directly.

// engine.io-client: Active but internal
// Prefer socket.io-client for application logic
import eio from 'engine.io-client';

ws, socket.io-client, primus, sockjs-client, and socketcluster-client are all actively maintained and suitable for production use.

// All actively maintained
import WebSocket from 'ws';
import { io } from 'socket.io-client';
// ... etc

📊 Summary: Key Differences

Featurewssocket.io-clientsockjs-clientprimus
ProtocolRaw WebSocketCustom (Socket.IO)SockJS ProtocolAgnostic
FallbacksNoneHTTP PollingXHR/JSONPConfigurable
ReconnectManualAutomaticAutomaticAutomatic
ServerAny WS ServerSocket.IO ServerSockJS ServerPrimus Server
Use CaseLow-level / NodeGeneral Web AppsFirewalls / CorpLibrary Authors
Featureengine.io-clientfaye-websocketsocketcluster-client
ProtocolEngine.IORFC WebSocketSocketCluster
FallbacksPollingNoneBuilt-in
ReconnectTransport LevelManualAutomatic
ServerEngine.IO ServerAny / FayeSocketCluster
Use CaseCustom ProtocolsLegacy / FayeSocketCluster Apps

💡 The Big Picture

socket.io-client is the default choice for most teams. It balances features, reliability, and ease of use. If you need rooms, namespaces, and guaranteed delivery without building them yourself, this is the tool.

ws is the choice for infrastructure engineers building custom protocols or Node.js-to-Node.js communication where fallbacks aren't needed. It is the foundation upon which many other libraries are built.

sockjs-client remains relevant for enterprise environments with strict proxy rules that block WebSocket upgrades. If your users are on corporate networks, this fallback capability is invaluable.

primus is for platform builders. If you are creating a service where you might want to swap the underlying WebSocket library later without breaking client code, Primus provides that abstraction layer.

socketcluster-client, engine.io-client, and faye-websocket are specialized tools. Use them only if your server architecture specifically demands them. For general greenfield development, stick to the broader ecosystem support of Socket.IO or raw WebSockets.

How to Choose: ws vs socket.io-client vs socketcluster-client vs engine.io-client vs faye-websocket vs primus vs sockjs-client

  • ws:

    Choose ws for low-level WebSocket communication in Node.js environments where you don't need fallbacks or complex rooms. It is the standard for server-side WebSocket implementation but is less common for direct browser usage compared to Socket.IO.

  • socket.io-client:

    Choose socket.io-client for most production real-time apps requiring reliability. It offers automatic reconnection, packet buffering, and fallback to HTTP polling if WebSockets are blocked. Best paired with the Socket.IO server for a complete, feature-rich ecosystem.

  • socketcluster-client:

    Choose socketcluster-client if your backend runs on the SocketCluster framework. It is tightly coupled to SocketCluster's pub/sub and worker architecture. Do not use this with generic Node.js WebSocket servers.

  • engine.io-client:

    Choose engine.io-client only if you are building a custom real-time protocol on top of Engine.IO or debugging Socket.IO internals. It is the transport layer for Socket.IO and lacks high-level features like rooms or namespaces. Direct usage is rare for general application development.

  • faye-websocket:

    Choose faye-websocket if you need a compliant WebSocket client for Node.js or browsers that adheres strictly to the RFC, often paired with the Faye pub/sub server. However, consider it legacy for new projects; modern alternatives offer better maintenance and ecosystem support.

  • primus:

    Choose primus if you want to decouple your client code from the underlying WebSocket library. It allows you to swap servers (e.g., from ws to uWebSockets.js) without changing client code. Ideal for libraries or platforms that need long-term stability despite underlying tech changes.

  • sockjs-client:

    Choose sockjs-client if you need WebSocket emulation with robust fallbacks (XHR-streaming, etc.) and want to remain protocol-agnostic on the server side. It is a solid choice for corporate environments where strict firewall rules often block standard WebSocket connections.

README for ws

ws: a Node.js WebSocket library

Version npm CI Coverage Status

ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and server implementation.

Passes the quite extensive Autobahn test suite: server, client.

Note: This module does not work in the browser. The client in the docs is a reference to a backend with the role of a client in the WebSocket communication. Browser clients must use the native WebSocket object. To make the same code work seamlessly on Node.js and the browser, you can use one of the many wrappers available on npm, like isomorphic-ws.

Table of Contents

Protocol support

  • HyBi drafts 07-12 (Use the option protocolVersion: 8)
  • HyBi drafts 13-17 (Current default, alternatively option protocolVersion: 13)

Installing

npm install ws

Opt-in for performance

bufferutil is an optional module that can be installed alongside the ws module:

npm install --save-optional bufferutil

This is a binary addon that improves the performance of certain operations such as masking and unmasking the data payload of the WebSocket frames. Prebuilt binaries are available for the most popular platforms, so you don't necessarily need to have a C++ compiler installed on your machine.

To force ws to not use bufferutil, use the WS_NO_BUFFER_UTIL environment variable. This can be useful to enhance security in systems where a user can put a package in the package search path of an application of another user, due to how the Node.js resolver algorithm works.

Legacy opt-in for performance

If you are running on an old version of Node.js (prior to v18.14.0), ws also supports the utf-8-validate module:

npm install --save-optional utf-8-validate

This contains a binary polyfill for buffer.isUtf8().

To force ws not to use utf-8-validate, use the WS_NO_UTF_8_VALIDATE environment variable.

API docs

See /doc/ws.md for Node.js-like documentation of ws classes and utility functions.

WebSocket compression

ws supports the permessage-deflate extension which enables the client and server to negotiate a compression algorithm and its parameters, and then selectively apply it to the data payloads of each WebSocket message.

The extension is disabled by default on the server and enabled by default on the client. It adds a significant overhead in terms of performance and memory consumption so we suggest to enable it only if it is really needed.

Note that Node.js has a variety of issues with high-performance compression, where increased concurrency, especially on Linux, can lead to catastrophic memory fragmentation and slow performance. If you intend to use permessage-deflate in production, it is worthwhile to set up a test representative of your workload and ensure Node.js/zlib will handle it with acceptable performance and memory usage.

Tuning of permessage-deflate can be done via the options defined below. You can also use zlibDeflateOptions and zlibInflateOptions, which is passed directly into the creation of raw deflate/inflate streams.

See the docs for more options.

import WebSocket, { WebSocketServer } from 'ws';

const wss = new WebSocketServer({
  port: 8080,
  perMessageDeflate: {
    zlibDeflateOptions: {
      // See zlib defaults.
      chunkSize: 1024,
      memLevel: 7,
      level: 3
    },
    zlibInflateOptions: {
      chunkSize: 10 * 1024
    },
    // Other options settable:
    clientNoContextTakeover: true, // Defaults to negotiated value.
    serverNoContextTakeover: true, // Defaults to negotiated value.
    serverMaxWindowBits: 10, // Defaults to negotiated value.
    // Below options specified as default values.
    concurrencyLimit: 10, // Limits zlib concurrency for perf.
    threshold: 1024 // Size (in bytes) below which messages
    // should not be compressed if context takeover is disabled.
  }
});

The client will only use the extension if it is supported and enabled on the server. To always disable the extension on the client, set the perMessageDeflate option to false.

import WebSocket from 'ws';

const ws = new WebSocket('ws://www.host.com/path', {
  perMessageDeflate: false
});

Usage examples

Sending and receiving text data

import WebSocket from 'ws';

const ws = new WebSocket('ws://www.host.com/path');

ws.on('error', console.error);

ws.on('open', function open() {
  ws.send('something');
});

ws.on('message', function message(data) {
  console.log('received: %s', data);
});

Sending binary data

import WebSocket from 'ws';

const ws = new WebSocket('ws://www.host.com/path');

ws.on('error', console.error);

ws.on('open', function open() {
  const array = new Float32Array(5);

  for (var i = 0; i < array.length; ++i) {
    array[i] = i / 2;
  }

  ws.send(array);
});

Simple server

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.on('error', console.error);

  ws.on('message', function message(data) {
    console.log('received: %s', data);
  });

  ws.send('something');
});

External HTTP/S server

import { createServer } from 'https';
import { readFileSync } from 'fs';
import { WebSocketServer } from 'ws';

const server = createServer({
  cert: readFileSync('/path/to/cert.pem'),
  key: readFileSync('/path/to/key.pem')
});
const wss = new WebSocketServer({ server });

wss.on('connection', function connection(ws) {
  ws.on('error', console.error);

  ws.on('message', function message(data) {
    console.log('received: %s', data);
  });

  ws.send('something');
});

server.listen(8080);

Multiple servers sharing a single HTTP/S server

import { createServer } from 'http';
import { WebSocketServer } from 'ws';

const server = createServer();
const wss1 = new WebSocketServer({ noServer: true });
const wss2 = new WebSocketServer({ noServer: true });

wss1.on('connection', function connection(ws) {
  ws.on('error', console.error);

  // ...
});

wss2.on('connection', function connection(ws) {
  ws.on('error', console.error);

  // ...
});

server.on('upgrade', function upgrade(request, socket, head) {
  const { pathname } = new URL(request.url, 'wss://base.url');

  if (pathname === '/foo') {
    wss1.handleUpgrade(request, socket, head, function done(ws) {
      wss1.emit('connection', ws, request);
    });
  } else if (pathname === '/bar') {
    wss2.handleUpgrade(request, socket, head, function done(ws) {
      wss2.emit('connection', ws, request);
    });
  } else {
    socket.destroy();
  }
});

server.listen(8080);

Client authentication

import { createServer } from 'http';
import { WebSocketServer } from 'ws';

function onSocketError(err) {
  console.error(err);
}

const server = createServer();
const wss = new WebSocketServer({ noServer: true });

wss.on('connection', function connection(ws, request, client) {
  ws.on('error', console.error);

  ws.on('message', function message(data) {
    console.log(`Received message ${data} from user ${client}`);
  });
});

server.on('upgrade', function upgrade(request, socket, head) {
  socket.on('error', onSocketError);

  // This function is not defined on purpose. Implement it with your own logic.
  authenticate(request, function next(err, client) {
    if (err || !client) {
      socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
      socket.destroy();
      return;
    }

    socket.removeListener('error', onSocketError);

    wss.handleUpgrade(request, socket, head, function done(ws) {
      wss.emit('connection', ws, request, client);
    });
  });
});

server.listen(8080);

Also see the provided example using express-session.

Server broadcast

A client WebSocket broadcasting to all connected WebSocket clients, including itself.

import WebSocket, { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.on('error', console.error);

  ws.on('message', function message(data, isBinary) {
    wss.clients.forEach(function each(client) {
      if (client.readyState === WebSocket.OPEN) {
        client.send(data, { binary: isBinary });
      }
    });
  });
});

A client WebSocket broadcasting to every other connected WebSocket clients, excluding itself.

import WebSocket, { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.on('error', console.error);

  ws.on('message', function message(data, isBinary) {
    wss.clients.forEach(function each(client) {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(data, { binary: isBinary });
      }
    });
  });
});

Round-trip time

import WebSocket from 'ws';

const ws = new WebSocket('wss://websocket-echo.com/');

ws.on('error', console.error);

ws.on('open', function open() {
  console.log('connected');
  ws.send(Date.now());
});

ws.on('close', function close() {
  console.log('disconnected');
});

ws.on('message', function message(data) {
  console.log(`Round-trip time: ${Date.now() - data} ms`);

  setTimeout(function timeout() {
    ws.send(Date.now());
  }, 500);
});

Use the Node.js streams API

import WebSocket, { createWebSocketStream } from 'ws';

const ws = new WebSocket('wss://websocket-echo.com/');

const duplex = createWebSocketStream(ws, { encoding: 'utf8' });

duplex.on('error', console.error);

duplex.pipe(process.stdout);
process.stdin.pipe(duplex);

Other examples

For a full example with a browser client communicating with a ws server, see the examples folder.

Otherwise, see the test cases.

FAQ

How to get the IP address of the client?

The remote IP address can be obtained from the raw socket.

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws, req) {
  const ip = req.socket.remoteAddress;

  ws.on('error', console.error);
});

When the server runs behind a proxy like NGINX, the de-facto standard is to use the X-Forwarded-For header.

wss.on('connection', function connection(ws, req) {
  const ip = req.headers['x-forwarded-for'].split(',')[0].trim();

  ws.on('error', console.error);
});

How to detect and close broken connections?

Sometimes, the link between the server and the client can be interrupted in a way that keeps both the server and the client unaware of the broken state of the connection (e.g. when pulling the cord).

In these cases, ping messages can be used as a means to verify that the remote endpoint is still responsive.

import { WebSocketServer } from 'ws';

function heartbeat() {
  this.isAlive = true;
}

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.isAlive = true;
  ws.on('error', console.error);
  ws.on('pong', heartbeat);
});

const interval = setInterval(function ping() {
  wss.clients.forEach(function each(ws) {
    if (ws.isAlive === false) return ws.terminate();

    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

wss.on('close', function close() {
  clearInterval(interval);
});

Pong messages are automatically sent in response to ping messages as required by the spec.

Just like the server example above, your clients might as well lose connection without knowing it. You might want to add a ping listener on your clients to prevent that. A simple implementation would be:

import WebSocket from 'ws';

function heartbeat() {
  clearTimeout(this.pingTimeout);

  // Use `WebSocket#terminate()`, which immediately destroys the connection,
  // instead of `WebSocket#close()`, which waits for the close timer.
  // Delay should be equal to the interval at which your server
  // sends out pings plus a conservative assumption of the latency.
  this.pingTimeout = setTimeout(() => {
    this.terminate();
  }, 30000 + 1000);
}

const client = new WebSocket('wss://websocket-echo.com/');

client.on('error', console.error);
client.on('open', heartbeat);
client.on('ping', heartbeat);
client.on('close', function clear() {
  clearTimeout(this.pingTimeout);
});

How to connect via a proxy?

Use a custom http.Agent implementation like https-proxy-agent or socks-proxy-agent.

Changelog

We're using the GitHub releases for changelog entries.

License

MIT