completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

WebSocket & Server-Sent Events

Learn long polling, WebSocket, Server-Sent Events, and how to choose a real-time transport without opening external network connections.

By the end, you can
  • 01
    Compare the four waysTell short polling, long polling, Server-Sent Events, and WebSocket apart by who sends and when.
  • 02
    Use the browser tools safelyName the WebSocket states and close codes, and read the text lines of a Server-Sent Events stream.
  • 03
    Choose the right onePick a way to get updates for chat, scores, notifications, games, and checking on a slow job.

Some pages change while you watch. A score goes up, or a chat message appears, and you never press refresh. Often the new data arrives as a short line of text from the server. Here is one such line.

One line of text from the serverPop out in the code editor (opens in a new tab)JavaScript
const line = "data: tea";
console.log(line.startsWith("data:"));
console.log(line.slice(6));

Line 1 stores one line of text: data: tea. Line 2 checks that the line starts with data:, so it prints true. Line 3 cuts off the first six characters, data: with its space, and prints tea.

Later in this lesson, the browser does this cutting for you. It hands your code the rest of the line, tea, as event.data. The value stays text. The browser does not turn it into JSON.

Real-time means “tell me when it happens”

So far, most network code you have seen follows one pattern, called request and response:

  • Your code asks the server for something. This is the request.
  • The server sends back one answer. This is the response.
  • Then that exchange is over.

This works well for loading a profile, saving a form, or getting JSON with fetch(). A real-time page is different. It shows new data as soon as the server has it, without a refresh.

JavaScript has four common ways to get these updates. This lesson covers each one:

  • Short polling: the page asks the server again and again, on a timer.
  • Long polling: the page asks once, and the server waits to answer until it has news.
  • Server-Sent Events (SSE): the server keeps sending text updates to the page.
  • WebSocket: the page and the server stay connected, and both can send messages at any time.

The four ways differ in a few things:

  • Who can send: only the server, or both sides.
  • Whether they reconnect by themselves after the connection drops.
  • Whether they can carry binary data: raw bytes, such as an image, instead of text.
  • How well they pass through a proxy. A proxy is a computer between the browser and the server that passes messages along.
  • How much code you have to write yourself.
Real-life analogyA live score page that updates itself

You follow a football match on your phone. You can refresh the page every minute. You can ask once and wait for the next goal. The score page can send you each score as it happens. Or you can chat with other fans while the scores arrive.

So the question is not “which way is newest?” Ask instead: who needs to send, how often, and what kind of data? A score page mostly listens. A game with many players sends both ways. A page that checks whether a report is ready may only need to ask once a minute.

In real life: You refresh the score every minute
In JavaScript: A timer calls fetch() again and again
In real life: You ask once and wait for the next score
In JavaScript: One request waits until there is news or time runs out. Then you ask again
In real life: The score page sends you each new score
In JavaScript: The server keeps sending text updates to the browser
In real life: You also send messages about the match
In JavaScript: The page and the server both send messages at any time over one open connection
The demos run inside this page

This site never connects to other servers. So the WebSocket and long-polling demos use a pretend server that runs inside this page. It uses the same event names and numbers as the real thing.

The SSE demo first tries the browser's real EventSource, with the stream text stored inside the page in a data: URL. If the browser blocks that, a small, tested function reads the same text instead.

Long polling: ask, wait for news, ask again

INTERACTIVE

Short polling is the simplest way. A timer asks the server every few seconds: “Anything new?” It works almost everywhere, because each ask is a normal HTTP request. HTTP is the same way of talking to a server that fetch() uses.

Short polling has two costs:

  • When nothing changes, the requests still go out and come back empty.
  • A new update has to wait until the next timer tick.

Long polling changes one thing. The browser sends a request, but the server does not answer right away. It holds the request until it has news, or until a time limit runs out. This time limit is called a timeout.

As soon as the answer arrives, the browser handles it and sends the next request. Each message still costs one request. But quiet times no longer fill up with empty “no change” answers.

When does each way get the update?Pop out in the code editor (opens in a new tab)JavaScript
const updateAt = 18;
const nextPoll = 20;
console.log(nextPoll - updateAt, "seconds late");
console.log("long polling: answer when the update arrives");

Line 1 says the update happens at second 18. Line 2 says the next short-polling request goes out at second 20. Line 3 subtracts the two and prints 2 seconds late. Line 4 describes long polling. Its request is already waiting at the server, so the server can answer the moment the update happens. These numbers are a made-up example, not a real network clock.

Top row, short polling: the update happens at 18s, but the answer comes at 20s, with the next ask. 2 seconds late. Bottom row, long polling: the update happens at 18s and the answer comes at 18s. 0 seconds late.short pollingupdate at 18sanswer at 20s2 seconds latelong pollingupdate at 18sanswer at 18s0 seconds late
The update happens at second 18. Short polling hears about it at its next ask, at second 20. Long polling's request is already waiting, so the answer goes out at once.
Short polling and long polling over time
The requests each way sendsHTTP-ish timeline
// Short polling: ask every 10 secondsGET /updates?since=0  -> no changeGET /updates?since=0  -> update at 20s // Long polling: ask once and waitGET /updates?since=0  -> server holds                         update at 18s
Timelineasks every 10s
0srequest returns no changeone request waits until 18s, then returns update
10srequest returns no changeno extra request yet
20srequest returns update after 2s delayno extra request yet
30srequest returns no changeno extra request yet
40srequest returns no changeno extra request yet
Try it yourself

Short polling sends 6 requests per minute. It can get the update 2s after it happens. Long polling keeps one request waiting, so in this example it gets the update at once.

Move the update time. Short polling waits for its next request. With long polling, the server answers the request that is already waiting.

Long polling is useful in two cases:

  • Old network equipment blocks connections that stay open, like WebSocket.
  • The server is already built to answer ordinary requests, one at a time.

Long polling code needs the same care as other retry code: a loop, a timeout, and error handling. You saw this kind of retry code in the Async patterns lesson.

WebSocket: one open, two-way conversation

STEP THROUGH

You create a WebSocket with new WebSocket(url, protocols):

  • The url starts with wss://, like "wss://example.com/chat".
  • The protocols argument is optional. It names the message format both sides agree to use, like "chat.v1".

First the browser and the server agree to connect. This first exchange is called the handshake. After it, both sides can send messages at any time. That is why chat, games with many players, shared documents, and other pages where the user also sends a lot usually use WebSocket.

A WebSocket is always in one of four states. It keeps its current state in a property called readyState, as a number from 0 to 3.

The open state is number 1Pop out in the code editor (opens in a new tab)JavaScript
const states = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
console.log(states[1]);
console.log(String(1));

Line 1 lists the four state names in number order. Line 2 prints states[1], the item at index 1, which is OPEN. Line 3 prints 1, the number for that state. The name and the number mean the same state. Printing them does not open anything, though. Only a real socket in the OPEN state can send.

Four cards in a row with arrows between them: 0 CONNECTING, 1 OPEN, 2 CLOSING, 3 CLOSED. Under CONNECTING: send() throws. Under OPEN: send() works. Under CLOSED: stays closed.0CONNECTING1OPEN2CLOSING3CLOSEDsend() throwssend() worksstays closed
A WebSocket moves through four states, numbered 0 to 3. Only in OPEN can send() send your message.

When a WebSocket closes, it reports a close code: a number that says why it closed.

Close codes say why the connection endedPop out in the code editor (opens in a new tab)JavaScript
const closeCodes = { normal: 1000, goingAway: 1001, abnormal: 1006 };
console.log(closeCodes.normal);
console.log(closeCodes.abnormal);

Line 1 stores three common close codes in an object. Line 2 prints 1000, the code for a normal close. Line 3 prints 1006, the code for a connection that vanished without a proper close. The browser can report 1006 to your code. But your code is not allowed to send 1006 itself.

Real-life analogyreadyState is like the state of a phone call

When you call someone, you can't talk right away. First the phone dials. Then the call connects, and only then can you talk.

WebSocket has the same rule: send only after the open event, when readyState is OPEN.

In real life: The phone is dialing
In JavaScript: CONNECTING, readyState 0
In real life: The call is connected
In JavaScript: OPEN, readyState 1; send() is allowed
In real life: You are hanging up
In JavaScript: CLOSING, readyState 2
In real life: The call has ended
In JavaScript: CLOSED, readyState 3
In real life: Asking ‘Still there?’ when the line goes quiet
In JavaScript: Heartbeat messages: ping and pong

Step through one WebSocket from start to end below: create it, wait for it to open, send a message, get messages back, and close it.

One WebSocket from start to end
Step 0 of 8Ready
Your turn: follow the blue line

This replay uses a pretend WebSocket, so nothing leaves the page. It uses the real WebSocket event names and readyState numbers.

Running in
  1. script
Next: line 1
 socket.addEventListener("open", () => {  socket.send("hello bots");}); socket.addEventListener("message", (event) => {  console.log(event.data);}); socket.addEventListener("close", (event) => {  console.log(event.code, event.reason);});
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

A few more facts matter in real apps:

  • send() takes text or binary data. Binary data can be a Blob, an ArrayBuffer, or a typed array. All three are ways to hold raw bytes.
  • Calling send() while the socket is still CONNECTING throws an error named InvalidStateError.
  • bufferedAmount is the number of bytes waiting to go out. You passed them to send(), but the browser has not sent them yet.
  • Close code 1000 means a normal close. 1001 means one side is going away, for example because the page is closing. 1006 means the connection vanished; only the browser reports it.
  • WebSocket never reconnects by itself.
  • If the page was loaded over HTTPS, use a wss:// URL. It is the secure kind.
A pretend chat room with WebSocket
The chat client codePop out in the code editor (opens in a new tab)JavaScript
const socket = new WebSocket("wss://example.test/chat", "chat.v1"); socket.addEventListener("open", () => {  socket.send("Hello room!");}); socket.addEventListener("message", (event) => {  appendToLog(event.data);}); socket.addEventListener("close", (event) => {  showCloseCode(event.code, event.reason);});
Pretend roomCLOSED
  1. Not connected. Press Connect to join the pretend room.
Try it yourself

Nothing leaves this page: the room is pretend. It uses the real WebSocket event names and readyState numbers. Current state: CLOSED.

Connect, then send a message. Let the server send to everyone. Drop the connection to see code 1006, then reconnect with backoff.

In the playground, the Drop connection button reports 1006, so you can see a connection vanish. The Reconnect with backoff button waits a little, then creates a new socket. Backoff means waiting longer after each failed try. Real apps also set a longest wait, check whether the device is online, and log the user in again when needed.

The wait doubles, up to a limitPop out in the code editor (opens in a new tab)JavaScript
function retryDelay(attempt) {
  return Math.min(8000, 250 * 2 ** attempt);
}
console.log(retryDelay(0));
console.log(retryDelay(3));

Line 1 starts a function that takes the attempt number. Line 2 works out the wait in milliseconds:

  • 2 ** attempt is 2 to the power of attempt: 1, 2, 4, 8, and so on. Times 250, that gives 250, 500, 1000, 2000. So the wait doubles with each attempt.
  • Math.min returns the smaller of its two numbers. So the wait never goes above 8000.

Line 4 prints 250 for attempt 0. Line 5 prints 2000 for attempt 3, because 250 × 2 × 2 × 2 is 2000. The limit matters during a long outage. Without it, the next wait could grow to hours.

A heartbeat is a tiny message that asks “Still there?” on a quiet connection. If no answer comes back, the app knows the connection is broken.

A heartbeat message your app sendsPop out in the code editor (opens in a new tab)JavaScript
const appHeartbeat = "ping";
console.log(appHeartbeat);
console.log("pong");

Line 1 picks the app's heartbeat message, "ping". Line 2 prints it. Line 3 prints the answer the app expects back, pong. These are normal app messages. The WebSocket protocol has its own ping signal too, but page JavaScript cannot send it.

A newer API

WebSocketStream is a newer way to use WebSockets, built on streams. At the time of writing, only Chromium-based browsers, such as Chrome and Edge, have it. Most code still uses WebSocket.

Server-Sent Events: a one-way text stream

INTERACTIVE

Server-Sent Events (SSE) let a server keep sending text to the page. In the browser, you use an object called EventSource. The server answers one normal HTTP request, but it never finishes the answer. It keeps adding text to it. The answer is marked with the type text/event-stream.

Messages go one way only: from the server to the browser. That is fine when the page mostly listens. In return, the browser does several jobs for you:

  • It reconnects by itself if the connection drops.
  • It tells your code the name of each event.
  • It remembers the id of the last event.
  • It follows the server's advice on how long to wait before reconnecting.

The stream is plain text, with one field on each line:

  • event: names the event. You listen for it with addEventListener(name, ...).
  • data: holds the message. Your code reads it as event.data.
  • id: sets lastEventId. When the browser reconnects, it sends this id back in a Last-Event-ID header. A header is a named note sent along with a request. It tells the server where to start again.
  • retry: says how many milliseconds to wait before reconnecting.
  • A blank line ends one event and sends it to your code.

An event with no event: line goes to onmessage. A named event does not. It only reaches a listener added for its own name.

Three lines of stream text on the left: id: goal-1, event: score, data: Owls 1, Foxes 0. An arrow from each line points to a value on the right. goal-1 becomes event.lastEventId, score becomes event.type, and Owls 1, Foxes 0 becomes event.data. Then a blank line sends the event.id: goal-1goal-1event.lastEventIdevent: scorescoreevent.typedata: Owls 1, Foxes 0Owls 1, Foxes 0event.datathen a blank line sends it
Each line of stream text fills one part of the event your code gets. The blank line after them sends the event.
Server-Sent Events without a server
The EventSource codePop out in the code editor (opens in a new tab)JavaScript
const source = new EventSource("data:text/event-stream,..."); source.addEventListener("score", (event) => {  console.log(event.lastEventId, event.data);}); source.onmessage = (event) => {  console.log("message", event.data);}; source.close(); // stop reconnecting
The stream texttext/event-stream
retry: 1200id: kickoffevent: scoredata: Owls 0, Foxes 0 id: goal-1event: scoredata: Owls 1, Foxes 0 id: note-1data: Halftime snacks are ready
Events receivedparser
  1. Press a button to get the events.
Try it yourself

Not started Mode: parser. The backup reader is a small function, tested outside the browser.

The real EventSource never contacts another server: the stream text is stored inside a data: URL. If the browser blocks that, a small function reads the same text instead.

SSE carries only text, but that text can be JSON. It fits notification lists, live sports scores, progress messages, logs, and dashboards. To send cookies with the request, write new EventSource(url, { withCredentials: true }).

One more limit: with the older HTTP/1.1, a browser keeps only a few connections open to one site, often six. Many tabs with SSE open can use them all up. The newer HTTP/2 usually avoids this, because many streams share one connection.

Choosing a transport

SORT IT

A transport is the way updates travel between the server and the page. To choose one, start with direction: who needs to send?

Three rows, each with a page card on the left and a server card on the right. Polling: first the page asks, then the server answers. SSE: one arrow from the server to the page; only the server sends. WebSocket: arrows both ways; both send any time.pollingpageserver1. page asks2. server answersSSEpageserveronly the server sendsWebSocketpageserverboth send any time
Polling: the page asks and the server answers. SSE: only the server sends. WebSocket: both sides send whenever they want.

The table compares the choices. Read down a column to learn one choice, or across a row to compare one question.

Polling, Server-Sent Events, and WebSocket side by side
QuestionPolling or long pollingServer-Sent EventsWebSocket
Who can send?The page asks, and the server answers.Only the server sends.Both sides send at any time.
What does it use?Normal HTTP requests.One HTTP answer of type text/event-stream that keeps going.A WebSocket connection. It starts as an HTTP request that asks to switch.
Does it reconnect by itself?No. Your loop decides.Yes. It uses retry and Last-Event-ID.No. Your app decides.
Can it carry binary data?Yes, as normal answers.No. It carries only text (UTF-8).Yes. It carries text and binary data.
Does it pass through proxies?Yes, very well.Mostly. Some proxies hold text back, and connection limits matter.Sometimes the proxy must support WebSocket first.
Where is it used?Checking a slow job, and as a backup plan.Scores, notifications, progress, and dashboards.Chat, games, shared editing, and live controls.
Choose the transport
  • Live sports scores: the server sends each new score, and the page only listens
  • A game with many players: every player sends moves and gets updates quickly
  • Check once a minute whether a report is finished
  • Chat room: people type at any time and get messages at any time
  • Notifications list: new items appear while the tab is open
  • Very old network equipment where proxies block connections that stay open
Try it yourself
0 of 6 correct

Put each situation under the simplest transport that fits who sends and how often.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

A simple rule: pick the simplest thing that does the job.

  • If updates can wait a while, use polling.
  • If the server sends text and the page only listens, start with SSE.
  • If both sides need to send at any moment, use WebSocket.
  • If the network equipment blocks these, long polling still works.

Where you will use this

In a real app, the code around the connection matters as much as the connection. It has to:

  • read each message,
  • update what the page shows,
  • show whether the connection is working,
  • close the connection when that part of the page goes away,
  • and avoid adding the same listener twice.

The examples below leave out framework code, so the connection code is easy to see.

A chat client with WebSocketJavaScript
const socket = new WebSocket("wss://example.com/chat", "chat.v1"); socket.addEventListener("open", () => {  socket.send(JSON.stringify({ type: "join", room: "lobby" }));}); socket.addEventListener("message", (event) => {  const message = JSON.parse(event.data);  renderChatMessage(message);}); socket.addEventListener("close", (event) => {  scheduleReconnect(event.code);});

Line 1 opens the socket. Line 4 runs once the socket is open and sends a join message as JSON text. Line 8 turns each message that arrives back into an object with JSON.parse. Line 13 runs when the socket closes and plans a new connection. renderChatMessage and scheduleReconnect stand for your own app's functions.

A notification list with EventSourceJavaScript
const feed = new EventSource("/notifications/stream", {  withCredentials: true,}); feed.addEventListener("notification", (event) => {  const item = JSON.parse(event.data);  addNotification(item);}); feed.onerror = () => {  showStatus("Reconnecting...");};

Lines 1 to 3 open the stream and send cookies with it. Line 5 listens for events named notification, and line 6 reads each one as JSON. Line 10 runs when the connection has a problem. The browser is already reconnecting by itself, so the code only shows a message.

Choosing a transport in codePop out in the code editor (opens in a new tab)JavaScript
function chooseRealtimeTransport({ clientTalks, binary, textUpdates, rareUpdates }) {  if (clientTalks || binary) return "WebSocket";  if (textUpdates) return "Server-Sent Events";  if (rareUpdates) return "Polling";  return "Long polling";}

Line 2 picks WebSocket when the page must send too, or when the data is binary. Line 3 picks Server-Sent Events for text updates from the server. Line 4 picks polling when updates are rare. Line 5 uses long polling for everything else.

One step is easy to forget: closing. When a part of the page no longer needs updates, call socket.close() or eventSource.close(). Otherwise a hidden tab, or a part of the page you already removed, keeps getting data and doing work.

Common misconceptions

  • “Real-time always means WebSocket.” If data only goes from the server to the browser, SSE is often simpler. It also reconnects by itself.
  • “SSE can replace every WebSocket.” SSE goes one way and carries only text. To send from the page, use a separate request, or use WebSocket.
  • “WebSocket reconnects by itself.” It does not. After the close event, that socket is finished. Your code must create a new one.
  • “I should send close code 1006.” No. 1006 is kept for the browser, to report a connection that vanished. Your code may not send it.
  • “Long polling is fake real-time.” It is a real, useful way to get updates, especially as a backup plan. Its cost is one full request for each message.
  • “A heartbeat fixes every network problem.” A heartbeat only finds a quiet connection that broke. It does not make sure messages arrive, and you still need retries.

Practice exercises

4 EXERCISES
Exercise 1 · Warm-upSend before the socket is open

Read the code and predict the one line it prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
class TinySocket {
  constructor() { this.readyState = 0; }
  send() {
    if (this.readyState === 0) throw new Error("InvalidStateError");
    console.log("sent");
  }
}
const socket = new TinySocket();
try {
  socket.send("hi");
} catch (error) {
  console.log(error.message);
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeName the open state

    Type the readyState number that means a WebSocket is open.

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeCount the data lines

      The array holds five lines of stream text. Predict the number the code prints: how many lines start with data:?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const lines = ["event: score", "data: Owls 1", "", "data: plain", ""];
      console.log(lines.filter((line) => line.startsWith("data:")).length);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · ChallengeChoose for a real-time feature

        The two true-or-false values describe a feature. Predict which transport the code prints.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const needsClientMessages = true;
        const binaryFrames = true;
        console.log(needsClientMessages && binaryFrames ? "WebSocket" : "SSE");

        Answer, then press Check. Spacing and letter case don’t matter.

          Check your understanding

          7 QUESTIONS
          Real-time JavaScript quiz · 7 questionsScore: first tries count
          1. Question 1 of 7Which sentence describes long polling?

            Choose an answer to see the explanation.

          2. Question 2 of 7What does this readyState lookup print?

            Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
            const states = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
            console.log(states[1]);

            Choose an answer to see the explanation.

          3. Question 3 of 7Which sentence about WebSocket is true?

            Choose an answer to see the explanation.

          4. Question 4 of 7Does this SSE text contain a named event?

            Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
            const stream = "event: score\ndata: 2-1\n\ndata: plain\n\n";
            console.log(stream.includes("event: score"));

            Choose an answer to see the explanation.

          5. Question 5 of 7Which job does EventSource do for you?

            Choose an answer to see the explanation.

          6. Question 6 of 7Which transport would you choose to send player moves in a game with many players?

            Choose an answer to see the explanation.

          7. Question 7 of 7What does this transport check print?

            Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
            const supportsBinary = { sse: false, websocket: true };
            console.log(supportsBinary.websocket ? "binary ok" : "text only");

            Choose an answer to see the explanation.

          Key takeaways

          • Short polling asks on a timer. Long polling sends one request, and the server waits to answer it.
          • WebSocket is one open, two-way connection. Send only when it is OPEN, and reconnect in your own code.
          • Server-Sent Events are a one-way text stream. They have named events, ids, and a retry delay, and they reconnect by themselves.
          • Choose by who sends, how often, text or binary, what the network allows, and what should happen when the connection fails.

          Real-time code means picking the simplest connection that reliably brings updates when users need them.

          Up next: XMLHttpRequest, the older way to make requests. You will still see it in old code.

          CompleteFrontend Clear concepts. Working examples.