Server-Sent Events (SSE): Real-time updates from server to client

   



When developing modern web applications, we often need to send real-time updates from the server to the user’s browser. Whether it’s push notifications, news feeds, order updates, or system events, we want to avoid cumbersome solutions like continuous polling.

Server-Sent Events (SSE) offers a simple, efficient and native way to establish a persistent one-way connection from the server to the client.

πŸ”— Do you like Techelopment? Check out the website for all the details!


What is SSE?

Server-Sent Events (SSE) is a standard introduced by HTML5 that allows a server to send automatic updates to a client's browser over a one-way HTTP connection. Unlike traditional polling or WebSockets, SSE is designed for scenarios where data flows only from the server to the client.


What is it for?

SSE is ideal for applications where the client needs to receive real-time notifications or updates, but does not need to continuously send data to the server. Some examples include:

  • Push notifications

  • Real-time news feed

  • Order or transaction updates

  • System monitoring (real-time dashboard)

  • Sending messages (one-way)


How does it work?

The operation of SSE is relatively simple:

  1. The client opens an HTTP connection using the EventSource object in JavaScript.

  2. The server keeps the connection open and sends data in SSE format whenever it has new information.

  3. The data is sent as plain text, with a specific format that includes fields such as data:id:, and event:.



Implementation example

Client (JavaScript):


const source = new EventSource('/events');

source.onmessage = function(event) {
  console.log("Message from the server:", event.data);
};

source.onerror = function(error) {
  console.error("SSE error:", error);
};


Server (Node.js):


app.get('/events', (req, res) => {
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
 
    res.write('data: Welcome!\n\n');
 
    setInterval(() => {
      const timestamp = new Date().toISOString();
      res.write(`data: ${timestamp}\n\n`);
    }, 5000);
});


Benefits of SSE

  • Simplicity: Easily integrated into modern browsers with just a few lines of code.

  • Native browser support: no external libraries needed.

  • Automatic reconnection handling: the client attempts to automatically reconnect in the event of a disconnection.

  • Efficiency: less overhead than polling or WebSockets in cases of server→client transmission only.


Common mistakes to avoid

πŸ”΄ Error 1: Not setting the correct header

❌ Wrong code (Node.js with Express):


app.get('/events', (req, res) => {
    // Missing correct headers
    res.send('data: message\n\n');
});

✅ Correct:


app.get('/events', (req, res) => {
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
 
    res.write('data: message\n\n');
});


πŸ”΄ Error 2: Close connection after each message

❌ Wrong code:


app.get('/events', (req, res) => {
    res.setHeader('Content-Type', 'text/event-stream');
    res.write('data: This is a message\n\n');
    res.end(); // ❌ Connection closed
});

✅ Correct:


app.get('/events', (req, res) => {
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
 
    setInterval(() => {
      res.write(`data: ${new Date().toISOString()}\n\n`);
    }, 5000);
});


πŸ”΄ Error 3: Do not handle reconnections and event IDs

❌ Wrong code:


// Server does not contain Last-Event-ID
app.get('/events', (req, res) => {
    res.setHeader('Content-Type', 'text/event-stream');
    res.write(`data: new data\n\n`);
});

✅ Correct:


app.get('/events', (req, res) => {
    res.setHeader('Content-Type', 'text/event-stream');
 
    const lastEventId = req.header('Last-Event-ID');
    if (lastEventId) {
      console.log('Resume from ID:', lastEventId);
    }
 
    const id = Date.now();
    res.write(`id: ${id}\n`);
    res.write(`data: new data with ID ${id}\n\n`);
});


πŸ”΄ Error 4: Send JSON data without serializing it

❌ Wrong code:


const data = { user: 'Luca', status: 'online' };
res.write(`data: ${data}\n\n`); // ❌ [object Object]

✅ Correct:


const data = { user: 'Luca', status: 'online' };
res.write(`data: ${JSON.stringify(data)}\n\n`);


πŸ”΄ Error 5: Using SSE for two-way communication

❌ Wrong code:


// Client tries to send messages via EventSource (not possible)
const source = new EventSource('/events');
source.send("Hello!"); // ❌ This method does not exist

✅ Correct:

If you want client→server communication, you have to use a separate HTTP request (but in that case we are not talking about Server-Sent Events anymore πŸ˜†):


// Client JS
fetch('/send-message', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ message: "Hello!" })
});


// Server
app.post('/send-message', (req, res) => {
    const msg = req.body.message;
    console.log("Received message:", msg);
    res.sendStatus(200);
});


πŸ” Consequences of keeping the connection open with SSE

⚠️ 1. Server resource consumption

  • Each open connection occupies a thread, socket, or process, depending on the technology and architecture used.

  • If you have thousands (or millions) of users connected at the same time, the load on your server can grow quickly.

✅ Solution: use non-blocking servers (like Node.js, Nginx with proxy pass, or Go) or technologies like Event Loop to handle many connections efficiently.

⚠️ 2. Proxies or load balancers time out

  • Many proxies or load balancers (e.g. Nginx, Apache, AWS ELB) automatically close HTTP connections that have been open for too long.

  • This can cause silent disconnections between client and server.

✅ Solution: send a SSE "keep-alive" message every X seconds (even just an empty comment)

⚠️ 3. Maximum connection limit per browser

  • Browsers impose a limit on the number of simultaneous connections per domain (typically 6).

  • An active SSE connection takes up one, which can limit the loading of other resources or requests.

✅ Solution: Be careful with your app design, avoid opening multiple EventSource in parallel from the same domain.

⚠️ 4. Limited compatibility with serverless environments

  • Serverless services like AWS Lambda or Vercel are not ideal for persistent connections, because they are designed for short requests.

✅ Solution: in these cases, evaluate alternatives such as WebSocket via dedicated services (e.g. AWS API Gateway WebSocket, Pusher, Ably, etc.)

⚠️ 5. Reconnection and Duplicate Management

  • When the connection is lost, EventSource automatically tries to reconnect. But if the server does not handle the id well, the client may lose or receive the same events twice.

✅ Solution: use the id: field in messages and read Last-Event-ID on arrival to ensure continuity.


SSE is simple and very useful, but it should be used with caution in large-scale or distributed environments. The key is to know its limitations and design the system robustly.



Follow me #techelopment

Official site: www.techelopment.it
facebook: Techelopment
instagram: @techelopment
X: techelopment
Bluesky: @techelopment
telegram: @techelopment_channel
whatsapp: Techelopment
youtube: @techelopment