WebSockets & Server-Sent Events

JavaScript · Lesson 48 of 61 9 min read Updated

With fetch, the browser always speaks first: it asks, the server answers, and the conversation ends. That is fine for loading a product list, but not for a live cricket score, a chat app, a delivery partner moving on a map or a share price that changes every second. For these, the server needs a way to send new data the moment it has it.

In this lesson you will learn two browser APIs for live data: WebSockets, a two-way connection that stays open, and Server-Sent Events, a simpler one-way stream from server to browser. You will also see the older polling techniques they replaced.

Ways to get live updates

TechniqueHow it worksDirectionStatus
PollingAsk with fetch every few secondsBrowser asksSimple but wasteful
Long pollingThe server holds each request open until it has newsBrowser asksOlder workaround
Server-Sent EventsOne long HTTP response that keeps sending eventsServer to browserCurrent, simple
WebSocketA permanent two-way connectionBoth waysCurrent, flexible

Polling with setInterval still works for slow-changing data, such as checking an exam result page every minute. But most of its requests come back with "nothing new", and updates arrive late. WebSockets and Server-Sent Events push updates instantly over one connection.

Opening a WebSocket

A WebSocket starts as a normal HTTP request and then "upgrades" into a connection that stays open. Either side can send a message at any time. WebSocket addresses start with wss:// (secure, like https://) or ws:// (unencrypted). Always use wss:// on real sites.

JavaScript
const socket = new WebSocket("wss://example.com/live");

The constructor returns immediately while the connection is made in the background. You react to four events:

EventFires when
openThe connection is ready; you can now send
messageThe server sent something; it is in event.data
errorSomething went wrong (the event gives no details, for security)
closeThe connection has ended; event.code and event.reason say why

The readyState property tells you where the connection is: 0 CONNECTING, 1 OPEN, 2 CLOSING, 3 CLOSED.

This example connects to a public echo server, a test server that sends back every message it receives. It is run by a third party, so if it happens to be down you will see the error message instead.

JavaScriptRun it yourself
const socket = new WebSocket("wss://ws.postman-echo.com/raw");
console.log("readyState:", socket.readyState); // 0 = CONNECTING

socket.addEventListener("open", () => {
  console.log("Connected, readyState:", socket.readyState);
  socket.send("Namaste from Pune");
  socket.send(JSON.stringify({ type: "score", runs: 146, wickets: 3 }));
});

let received = 0;
socket.addEventListener("message", (event) => {
  console.log("Server sent:", event.data);
  received++;
  if (received === 2) socket.close(1000, "Finished");
});

socket.addEventListener("close", (event) => {
  console.log("Closed with code", event.code, "clean:", event.wasClean);
});

socket.addEventListener("error", () => {
  console.log("Could not connect (the test server may be down)");
});
Output
readyState: 0
Connected, readyState: 1
Server sent: Namaste from Pune
Server sent: {"type":"score","runs":146,"wickets":3}
Closed with code 1000 clean: true

send() accepts a string, a Blob or binary data such as an ArrayBuffer. Objects must be turned into text with JSON.stringify() first. Calling send() before the open event throws an InvalidStateError, so always send from inside the open listener or check socket.readyState === WebSocket.OPEN.

Designing messages

A WebSocket carries plain messages with no built-in meaning, so apps agree on a small format. The common choice is JSON with a type field that says what kind of message it is:

JavaScript
const socket = new WebSocket("wss://chat.example.com/rooms/class-10b");

socket.addEventListener("open", () => {
  socket.send(JSON.stringify({ type: "join", name: "Meera" }));
});

socket.addEventListener("message", (event) => {
  let msg;
  try {
    msg = JSON.parse(event.data);
  } catch {
    return; // ignore anything that is not valid JSON
  }

  switch (msg.type) {
    case "chat":
      showMessage(msg.from, msg.text); // use textContent inside, never innerHTML
      break;
    case "typing":
      showTyping(msg.from);
      break;
    case "left":
      showNotice(`${msg.from} left the room`);
      break;
  }
});

function sendChat(text) {
  if (socket.readyState === WebSocket.OPEN) {
    socket.send(JSON.stringify({ type: "chat", text }));
  }
}

Treat every incoming message as untrusted: parse it inside try, check its type, and insert text with textContent so a message cannot inject HTML into your page (see Best Practices & Security).

For binary messages, such as audio chunks or game state, set socket.binaryType = "arraybuffer" to receive an ArrayBuffer instead of the default Blob (see Typed Arrays, Blobs & Base64).

Closing and reconnecting

Call socket.close(code, reason) when you are finished. Code 1000 means a normal close; you may also use your own codes from 3000 to 4999. The close event reports the code the connection ended with:

CodeMeaning
1000Normal closure
1001Going away, for example the server is restarting or the user left the page
1006Closed abnormally with no close message, for example the network dropped
1011The server hit an unexpected error

Connections drop all the time on mobile networks, so real apps reconnect automatically. Wait a little longer after each failure (this is called exponential backoff) so that thousands of users do not all hammer a recovering server at the same moment:

JavaScript
function connect(attempt = 0) {
  const socket = new WebSocket("wss://example.com/live-score");

  socket.addEventListener("open", () => {
    attempt = 0; // connected, so reset the counter
  });

  socket.addEventListener("message", (event) => {
    updateScoreboard(JSON.parse(event.data));
  });

  socket.addEventListener("close", (event) => {
    if (event.code === 1000) return; // we closed it on purpose
    const delay = Math.min(1000 * 2 ** attempt, 30000); // 1s, 2s, 4s ... max 30s
    setTimeout(() => connect(attempt + 1), delay);
  });
}

connect();

WebSockets need a server that understands the WebSocket protocol. In Node.js the ws package is a common choice, and many developers use the Socket.IO library, which adds rooms and automatic reconnection. Note that Socket.IO uses its own protocol on top, so a Socket.IO server needs the Socket.IO client, not a plain WebSocket.

Server-Sent Events

Often you only need updates in one direction: scores, notifications, progress of a long job, or text appearing word by word from an AI chatbot. Server-Sent Events (SSE) are made for this. The browser opens a normal HTTP request with EventSource, and the server keeps the response open, writing events into it as they happen.

The server sends plain text with the content type text/event-stream. Each event is one or more field: value lines followed by a blank line:

Text
id: 1
data: {"over": 17.1, "runs": 142, "wickets": 3}

id: 2
data: {"over": 17.2, "runs": 146, "wickets": 3}

id: 3
event: wicket
data: {"batter": "Kabir", "out": "caught"}

: a line starting with a colon is a comment

id: 4
data: {"over": 17.4, "runs": 146, "wickets": 4}

The data field holds the message, event gives it a name (without one, it is a message event), id labels it, and an optional retry line tells the browser how many milliseconds to wait before reconnecting. On the page:

JavaScript
const source = new EventSource("/live-score");

source.addEventListener("message", (event) => {
  const score = JSON.parse(event.data);
  console.log(`Over ${score.over}: ${score.runs}/${score.wickets}`);
});

source.addEventListener("wicket", (event) => {
  const { batter, out } = JSON.parse(event.data);
  console.log(`WICKET! ${batter} is ${out}`);
});

source.addEventListener("error", () => {
  console.log("Connection lost, readyState", source.readyState);
});

With the stream above, the console shows:

Output
Over 17.1: 142/3
Over 17.2: 146/3
WICKET! Kabir is caught
Over 17.4: 146/4

Named events need addEventListener with their own name; the message listener does not receive them. The best feature of EventSource is that reconnection is built in. If the connection drops, the browser waits and reconnects by itself (readyState goes back to 0 while it tries), and it sends the last id it saw in a Last-Event-ID request header, so the server can continue from where the user left off. Call source.close() to stop for good.

EventSource only makes GET requests and cannot set custom headers, but it sends cookies for the same site, so normal login sessions work. It sends text only, not binary data.

Choosing between them

  • Use Server-Sent Events when only the server talks: live scores, notifications, dashboards, streamed AI replies. They work with ordinary HTTP servers, proxies and logins, and they reconnect automatically.
  • Use WebSockets when both sides talk often and quickly: chat, multiplayer games, collaborative editing, live cursors.
  • Use plain fetch (with polling, if needed) when updates are rare.

Common mistakes

  • Sending before the connection opens. Wait for the open event.
  • Sending objects directly. socket.send({ a: 1 }) sends the text "[object Object]"; use JSON.stringify().
  • Trusting incoming data. Parse inside try, check the message type, and never put it into innerHTML.
  • Not reconnecting. WebSockets do not reconnect on their own; EventSource does.
  • Expecting a message listener to get named SSE events. Listen for each event name.
  • Using ws:// on an https:// page. Browsers block it as mixed content; use wss://.

Key points

  • new WebSocket("wss://...") opens a two-way connection; use the open, message, error and close events.
  • send() sends strings or binary data; send JSON with a type field and parse it on arrival.
  • Close with socket.close(1000) and reconnect after unexpected closes with increasing delays.
  • new EventSource(url) receives a one-way stream of server events over plain HTTP.
  • SSE events have data, optional event names and ids, and reconnect automatically with Last-Event-ID.
  • Choose SSE for server-to-browser updates and WebSockets for frequent two-way messages.

Exercise

Using the echo server from the lesson (wss://ws.postman-echo.com/raw), write a tiny "chat" in the Try-it editor. When the connection opens, send three JSON messages from three friends (Aarav, Isha and Rohan) with a type of "chat". For each message that comes back, parse it and print Name: text. After all three have returned, close the connection with code 1000 and print "Chat closed".

Show solution
JavaScript
const socket = new WebSocket("wss://ws.postman-echo.com/raw");
const outgoing = [
  { type: "chat", from: "Aarav", text: "Match at 4 pm?" },
  { type: "chat", from: "Isha", text: "Yes, at the Rohtak stadium" },
  { type: "chat", from: "Rohan", text: "I will bring the ball" },
];
let count = 0;

socket.addEventListener("open", () => {
  for (const msg of outgoing) socket.send(JSON.stringify(msg));
});

socket.addEventListener("message", (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === "chat") console.log(`${msg.from}: ${msg.text}`);
  if (++count === outgoing.length) socket.close(1000);
});

socket.addEventListener("close", () => console.log("Chat closed"));
socket.addEventListener("error", () => console.log("Could not connect"));
Output
Aarav: Match at 4 pm?
Isha: Yes, at the Rohtak stadium
Rohan: I will bring the ball
Chat closed

All three sends happen inside the open listener, so none are sent too early. Counting the replies tells the code when every message has come back.

Quick Quiz

1. Which API gives a two-way connection where both browser and server can send at any time?

2. What happens if you call socket.send() before the open event?

3. Which feature does EventSource have that WebSocket does not?

4. A server sends an SSE event with the line event: wicket. How do you receive it?

Learn JavaScript with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call