Learn WebSocket - Presence & Heartbeat Mechanisms
Episode 11 of 34

Learn WebSocket - Presence & Heartbeat Mechanisms

This episode covers keeping connections healthy: the ping-pong mechanism, heartbeat intervals, dead connection detection, and user presence such as online, offline, and last-active status.

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

Introduction

A WebSocket connection that looks healthy may already be dead: the network drops midway, the device sleeps, or the router is evicted because of no traffic. Without checks, the server will never know until the user tries to send a message and fails.

Episode 11 covers heartbeat: the ping-pong mechanism to ensure a connection is alive, and presence: the server's knowledge of who is online, offline, busy, or long inactive. This is the foundation of apps like chat that show green statuses and gray dots.

The Ping-Pong Mechanism

WebSocket Control Frames

The WebSocket protocol has the ping and pong control frames. Either the client or the server sends a ping, and the counterpart answers with a pong. If there is no pong, the connection is considered dead.

JSManual ping-pong in the browser client
ws.onopen = () => {
  heartbeat();
};
 
function heartbeat() {
  clearTimeout(ws.pingTimeout);
  ws.pingTimeout = setTimeout(() => {
    console.log("tidak ada pong, menutup koneksi");
    ws.close();
  }, 30000);
}
 
ws.onpong = () => {
  console.log("pong diterima, koneksi sehat");
  heartbeat();
};

In the browser there is no API to send a ping manually, so the example above relies on the browser's automatic pong as a marker of a live connection. The 30-second timer restarts every time a pong is received.

Heartbeat on the ws Server

On the Node.js server side with the ws library, pings can be sent directly.

JSHeartbeat on the ws server
const wss = new WebSocketServer({ port: 8080 });
 
function heartbeat() {
  clearTimeout(this.isAlive);
  this.isAlive = setTimeout(() => this.terminate(), 30000);
}
 
wss.on("connection", (ws) => {
  ws.isAlive = null;
  heartbeat.call(ws);
  ws.on("pong", () => heartbeat.call(ws));
});
 
setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) return;
    ws.ping();
  });
}, 10000);

Every 10 seconds the server sends ws.ping() to all clients. If there is no pong within 30 seconds, ws.terminate() cuts off the connection considered dead. This isAlive pattern, reset by the pong, is the standard in the ws documentation.

Presence Detection

User Status

Presence is information above the connection level: not just whether the connection is alive, but what the user is doing.

JSTracking presence status
const pengguna = new Map();
 
io.on("connection", (socket) => {
  socket.on("presence:set", (status) => {
    pengguna.set(socket.userId, {
      status,
      terakhirAktif: Date.now(),
    });
    io.emit("presence:update", {
      userId: socket.userId,
      status,
    });
  });
 
  socket.on("disconnect", () => {
    pengguna.set(socket.userId, {
      status: "offline",
      terakhirAktif: Date.now(),
    });
  });
});

The pengguna Map stores the status per user. When the connection drops, the status changes to offline automatically. The terakhirAktif field can power text like "active 5 minutes ago".

Idle Detection

Presence is not only about the connection — you can also track client activity.

JSSetting status to away when idle
let idleTimer;
 
socket.on("input:activity", () => {
  clearTimeout(idleTimer);
  setStatus("online");
  idleTimer = setTimeout(() => setStatus("away"), 120000);
});

The 120-second timer resets every time the user is active. If there is no activity, the status changes to away. Combining a live connection and activity produces far more accurate presence.

Presence Broadcasting

Distributing Status Changes

Presence changes must be broadcast so all clients show the latest status.

JSBroadcasting a status change
function setStatus(status) {
  socket.emit("presence:me", { status });
  socket.to("room:utama").emit("presence:user", {
    userId: socket.userId,
    status,
  });
}

socket.to("room:utama") ensures only room members receive the update. When entering the app, the client also requests the status list of all users to render the initial state.

Presence Scalability

On a single server, a simple Map is enough. When servers multiply, presence must be stored in a shared place such as Redis so every instance sees the same status. We will cover this approach more deeply in episode 16.

Socket.IO Heartbeat Configuration

Socket.IO already handles ping-pong internally with two main options.

JSPing configuration in Socket.IO
const io = new Server(server, {
  pingInterval: 25000,
  pingTimeout: 20000,
});

pingInterval sets how often the server sends a ping, and pingTimeout the time limit to wait for a pong before the connection is terminated. The total above means a dead connection is detected within a maximum of 45 seconds. These values can be tuned to your users' network conditions.

Closing

Episode 11 gave you the ability to detect connections that are actually dead and to show accurate presence. Heartbeat prevents resource leaks; presence makes the application feel alive and responsive.

Key takeaways:

  • Ping-pong frames ensure a seemingly healthy connection is really alive.
  • After a timeout with no pong, the connection must be closed or terminated.
  • Presence tracks user status above the connection level.
  • Statuses can be online, offline, away, and last-active.
  • Presence changes are broadcast only to the relevant room.
  • Socket.IO provides pingInterval and pingTimeout out of the box.

In the next episode we cover error handling & reconnection strategies: the types of connection failures, exponential backoff, Socket.IO reconnection configuration, and state recovery after the connection returns.

Learn WebSocket - Presence & Heartbeat Mechanisms | Learn WebSocket