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.
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.
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.
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.
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();
socket.io includes first-class support for grouping connections:
/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);
}
});
}
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);
}
});
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.
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.
socket.io-client with socket.io server.WebSocket or ws client with ws server.| Feature | socket.io | ws | uws |
|---|---|---|---|
| Status | ✅ Actively maintained | ✅ Actively maintained | ❌ Deprecated |
| Protocol | Custom (WebSocket + fallbacks) | RFC 6455 WebSocket | RFC 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 |
| Overhead | Higher (metadata, protocol framing) | Minimal (raw frames) | Low (but unmaintained) |
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.
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.
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.
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.
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.
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.
protocolVersion: 8)protocolVersion: 13)npm install ws
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.
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.
See /doc/ws.md for Node.js-like documentation of ws classes and
utility functions.
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
});
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);
});
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);
});
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');
});
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);
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);
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.
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 });
}
});
});
});
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);
});
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);
For a full example with a browser client communicating with a ws server, see the examples folder.
Otherwise, see the test cases.
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);
});
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);
});
Use a custom http.Agent implementation like https-proxy-agent or
socks-proxy-agent.
We're using the GitHub releases for changelog entries.