Learn WebSocket - WebSocket Server with Node.js (ws library)
Episode 5 of 34

Learn WebSocket - WebSocket Server with Node.js (ws library)

This episode builds a WebSocket server with the ws library: installation, server creation, the connection and message events, broadcasting to all clients, and connection management with identification and cleanup.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

In episode 4 you got a client. Now it is time to build the serving side: the WebSocket server. We use ws, the most popular Node.js library for this protocol.

The ws library is lightweight and close to the raw protocol — no automatic reconnection layer or rooms. That is precisely its advantage: you will understand how WebSocket actually works before layering on the convenience of Socket.IO in the next episode. In this episode you will create a server, handle events, broadcast messages, and manage many clients.

Installation and Server Setup

Creating the Project and Installing ws

Start from an empty project:

Install the ws library
mkdir ws-server
cd ws-server
npm init -y
npm install ws

The command npm install ws pulls in the library along with its minimal dependencies. After this, you are ready to write your first server.

The Most Basic WebSocket Server

JSFirst WebSocket server
const { WebSocketServer } = require("ws");
 
const wss = new WebSocketServer({ port: 8080 });
 
wss.on("connection", (ws) => {
  console.log("klien terhubung");
  ws.send("selamat datang");
});

new WebSocketServer({ port: 8080 }) creates a server that listens for connections on port 8080. The connection event is fired every time a handshake completes, carrying the ws object belonging to that client.

Event Handling on the Server

The connection, message, close, and error events

The server communicates through events. Here are the handlers you must know:

JSComplete event handling
wss.on("connection", (ws) => {
  ws.on("message", (data, isBinary) => {
    console.log("terima:", data.toString());
    ws.send("pesan diterima");
  });
 
  ws.on("close", () => {
    console.log("klien pergi");
  });
 
  ws.on("error", (err) => {
    console.error("error:", err.message);
  });
 
  ws.on("pong", () => {
    console.log("pong diterima");
  });
});

ws.on("message", handler) is called for every data frame. The isBinary argument distinguishes text and binary frames. The pong event is only active if the server uses a heartbeat — covered in a later episode.

The Difference Between Text and Binary Data

Because ws can receive two data types:

JSDistinguishing text and binary
ws.on("message", (data, isBinary) => {
  if (isBinary) {
    console.log("data biner:", data);
  } else {
    console.log("teks:", data.toString());
  }
});

Checking isBinary determines how to interpret data, which in ws is a Buffer for binary and a Buffer or string for text.

Sending and Broadcasting

Unicast: Sending to One Client

To send to a specific client, you need to keep a reference to it:

JSUnicast and broadcast
const wss = new WebSocketServer({ port: 8080 });
const klien = new Map();
 
wss.on("connection", (ws) => {
  klien.set(ws, new Date());
 
  ws.on("message", (data) => {
    const pesan = data.toString();
    for (const [k] of klien) {
      k.send(pesan);
    }
  });
});

Looping for (const [k] of klien) and calling k.send(pesan) on every entry is manual broadcasting. Since there is no broadcast API in ws, you build it yourself — exactly what Socket.IO will make easier.

Storing and Cleaning Up Clients

Without cleanup, already-disconnected clients will sit idle in memory:

JSCleaning up disconnected clients
ws.on("close", () => {
  klien.delete(ws);
  console.log("klien tersisa:", klien.size);
});

Always call klien.delete(ws) in the close event. The size of klien.size is also useful as a metric for active connection counts — the seed of the monitoring covered in a later episode.

Configuration and Limitations

Server Configuration Options

WebSocketServer accepts various options:

  • port or server: a standalone port or an existing HTTP server.
  • maxPayload: the payload size limit, for example 100e6 for 100MB.
  • perMessageDeflate: enables permessage-deflate compression.
  • clientTracking: tracks the list of clients in wss.clients.

Combining it with an HTTP server brings a big advantage: a single port serves both the REST API and WebSocket.

JSHTTP and WebSocket servers together
const http = require("http");
const { WebSocketServer } = require("ws");
 
const server = http.createServer((req, res) => {
  res.writeHead(200);
  res.end("server jalan");
});
 
const wss = new WebSocketServer({ server });
 
wss.on("connection", (ws) => {
  ws.send("halo dari port yang sama");
});
 
server.listen(8080);

With new WebSocketServer({ server }), one HTTP server on port 8080 serves both. This is the pattern production applications use.

Tip

Always set maxPayload. Without a limit, a client can send a giant payload and eat up server memory — one of the DoS vectors we will discuss in a later episode.

Closing

Episode 5 gave you a functional WebSocket server: ws installation, server creation, handling the connection and message events, manual unicast and broadcast, and client-list management that stays free of memory leaks.

Key takeaways:

  • The ws library is lightweight and close to the raw RFC 6455 protocol.
  • Key events: connection, message, close, error, and pong.
  • Broadcasting must be built manually by tracking the client list.
  • Always remove disconnected clients to prevent memory leaks.
  • Combine the HTTP and WebSocket servers for a single port.
  • Set maxPayload from the start for security.

In the next episode we introduce Socket.IO — the library that layers WebSocket with automatic reconnection, fallback, rooms, and namespaces. You will see when to use Socket.IO and when to stick with plain ws.