ws vs socket.io vs uws
Real-time WebSocket Communication Libraries for Node.js
wssocket.iouws
Real-time WebSocket Communication Libraries for Node.js

socket.io, uws, and ws are all npm packages that enable real-time, bidirectional communication between clients and servers using WebSockets or similar protocols. ws is a minimal, standards-compliant WebSocket implementation for Node.js. socket.io builds on top of WebSocket-like transports to provide additional features like automatic reconnection, rooms, and fallbacks for older browsers. uws was an alternative high-performance WebSocket server library, but it has been deprecated and is no longer maintained.

Npm Package Weekly Downloads Trend
3 Years
Github Stars Ranking
Stat Detail
Package
Downloads
Stars
Size
Issues
Publish
License
ws148,546,56122,655148 kB5a month agoMIT
socket.io10,379,28862,8651.42 MB2032 months agoMIT
uws65,8719,000-485 years ago-

Real-time Communication in Node.js: socket.io vs uws vs ws

When building real-time web applications — like chat apps, live dashboards, or multiplayer games — choosing the right WebSocket library is critical. The three packages under review (socket.io, uws, and ws) represent different philosophies: high-level convenience, raw performance (now deprecated), and minimal standards compliance. Let’s examine how they differ in practice.

⚠️ Deprecation Status: One Package Is Off the Table

Before diving into features, it’s essential to address maintenance status:

  • uws is deprecated. The author archived the GitHub repository and unpublished the package from npm. According to the official GitHub repo, the project is no longer maintained, and users are advised to consider alternatives. Do not use uws in new projects.

This leaves us with socket.io and ws as viable options — one opinionated and feature-rich, the other lean and standards-focused.

📡 Protocol and Transport Layer

socket.io does not strictly implement the WebSocket protocol. Instead, it uses a custom protocol layered over WebSocket (or HTTP long-polling as a fallback). This allows it to support older browsers and add features like acknowledgments and binary support with metadata.

// socket.io: client and server speak a custom protocol
const io = require('socket.io')(server);

io.on('connection', (socket) => {
  socket.emit('welcome', { message: 'Hello!' });
  socket.on('chat', (data) => {
    // Handle message
  });
});

ws implements the RFC 6455 WebSocket standard exactly. There’s no fallback transport — if the client doesn’t support WebSockets, it won’t work. This keeps the protocol lean and interoperable with any standards-compliant client.

// ws: pure WebSocket standard
const WebSocket = require('ws');
const wss = new WebSocket.Server({ server });

wss.on('connection', (ws) => {
  ws.send(JSON.stringify({ message: 'Hello!' }));
  ws.on('message', (data) => {
    const msg = JSON.parse(data);
    // Handle message
  });
});

uws (deprecated) also aimed for RFC 6455 compliance but used a different C++ backend for performance. However, since it’s unmaintained, code examples are omitted, and migration is strongly recommended.

🔌 Connection Management and Reconnection

socket.io handles disconnections and reconnections automatically. The client retries with exponential backoff, and the server tracks session state (like rooms) across reconnects using a unique ID.

// socket.io: automatic reconnection (client-side)
const socket = io('http://localhost:3000', {
  reconnection: true,
  reconnectionAttempts: Infinity
});

ws provides no built-in reconnection logic. You must implement retry strategies, connection health checks, and state recovery yourself.

// ws: manual reconnection
function connect() {
  const ws = new WebSocket('ws://localhost:8080');
  ws.on('close', () => setTimeout(connect, 1000)); // simple retry
}
connect();

🏠 Rooms, Namespaces, and Broadcast

socket.io includes first-class support for grouping connections:

  • Rooms: Dynamically group sockets to broadcast messages.
  • Namespaces: Separate channels under the same server (e.g., /admin, /chat).
// socket.io: rooms and broadcast
io.on('connection', (socket) => {
  socket.join('room1');
  socket.to('room1').emit('msg', 'To room only');
  io.emit('msg', 'To all'); // broadcast to everyone
});

ws has no concept of rooms or namespaces. You must build your own mapping of connections to groups using Set or similar data structures.

// ws: manual room management
const rooms = new Map();

wss.on('connection', (ws) => {
  const roomId = 'room1';
  if (!rooms.has(roomId)) rooms.set(roomId, new Set());
  rooms.get(roomId).add(ws);

  ws.on('close', () => {
    rooms.get(roomId).delete(ws);
  });
});

// Broadcast to room
function broadcast(roomId, message) {
  const room = rooms.get(roomId);
  if (room) room.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

📦 Message Acknowledgment and Events

socket.io supports event-based messaging with optional callbacks for acknowledgment:

// socket.io: acknowledgment
socket.emit('update', data, (response) => {
  console.log('Server confirmed:', response);
});

// Server
socket.on('update', (data, callback) => {
  // process...
  callback({ success: true });
});

ws only sends raw messages. To implement acknowledgments, you’d need to design your own message format with IDs and response tracking.

// ws: manual ack system
const pendingAcks = new Map();

ws.on('message', (data) => {
  const msg = JSON.parse(data);
  if (msg.type === 'request') {
    // send response with same id
    ws.send(JSON.stringify({ id: msg.id, type: 'response' }));
  } else if (msg.type === 'response') {
    const resolve = pendingAcks.get(msg.id);
    if (resolve) resolve(msg);
  }
});

🧪 Browser and Client Compatibility

socket.io works in virtually all browsers, including very old ones, because it falls back to HTTP long-polling when WebSockets aren’t available.

ws requires a WebSocket-capable client. Modern browsers support it, but you lose compatibility with legacy environments unless you add your own fallback layer.

🔄 Interoperability

Because socket.io uses a custom protocol, a socket.io client cannot communicate with a ws server, and vice versa. You must use matching client and server libraries.

  • Use socket.io-client with socket.io server.
  • Use native WebSocket or ws client with ws server.

📊 Summary: Key Differences

Featuresocket.iowsuws
Status✅ Actively maintained✅ Actively maintained❌ Deprecated
ProtocolCustom (WebSocket + fallbacks)RFC 6455 WebSocketRFC 6455 (unmaintained)
Reconnection✅ Built-in❌ Manual❌ (Not applicable)
Rooms / Broadcast✅ Built-in❌ Manual❌ (Not applicable)
Acknowledgments✅ Built-in❌ Manual❌ (Not applicable)
Browser Fallbacks✅ Long-polling❌ None❌ None
OverheadHigher (metadata, protocol framing)Minimal (raw frames)Low (but unmaintained)

💡 When to Use What

  • Use socket.io when you’re building a real-time app quickly and need reliability across networks and browsers. Great for chat apps, collaboration tools, or live notifications where developer velocity matters more than micro-optimizations.

  • Use ws when you’re building a high-throughput service (like a game server or financial feed) where every byte and millisecond counts, and you’re comfortable managing connection logic yourself.

  • Never use uws in new code. If you inherit a project using it, plan a migration to ws or socket.io.

🛠️ Migration Tip

If you’re moving from uws to ws, the APIs are somewhat similar. Replace uWS.App().ws() with new WebSocket.Server(), and adjust message handlers to use on('message') instead of message callbacks. Remember to handle backpressure and connection limits explicitly, as ws doesn’t auto-throttle.

In the end, both socket.io and ws are excellent choices — just for different jobs. Pick the one that matches your team’s needs for control versus convenience.

How to Choose: ws vs socket.io vs uws
  • ws:

    Choose ws if you need a lightweight, standards-compliant WebSocket implementation with minimal overhead and full control over the connection lifecycle. It’s best suited for performance-sensitive applications where you can manage reconnection logic, message serialization, and browser compatibility yourself, and don’t require Socket.IO’s extra features.

  • socket.io:

    Choose socket.io if you need built-in features like automatic reconnection, rooms/namespaces, message acknowledgment, and cross-browser compatibility with HTTP long-polling fallbacks. It’s ideal for applications requiring rapid development of real-time features without managing low-level connection logic, though it adds protocol overhead compared to raw WebSockets.

  • uws:

    Do not use uws in new projects — it has been officially deprecated by its author and is no longer maintained. The package was removed from npm due to licensing and maintenance concerns, and the repository is archived. Existing projects should migrate to alternatives like ws or socket.io.

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