In this tutorial, I'll show you how to implement Server-Sent Events (SSE) in your frontend application. SSE is a simple and effective way to receive real-time updates from your server without the overhead of WebSockets or the inefficiency of polling.
What are Server-Sent Events?
Server-Sent Events are a standard that allows a web page to get updates from a server. Unlike WebSockets, SSE is a one-way communication channel - the server sends data to the client, but not vice versa. This makes SSE perfect for scenarios like:
- Live feeds and notifications
 - Real-time dashboards
 - Status updates
 - Chat applications (for receiving messages)
 - Stock tickers
 
SSE has several advantages over alternatives:
- Simpler than WebSockets - easier to implement on both client and server
 - Automatic reconnection - built into the protocol
 - Native browser support - no additional libraries needed
 - Regular HTTP - works with standard proxies and firewalls
 - Text-based - easy to debug
 
Browser Support
SSE is supported in all modern browsers including Chrome, Firefox, Safari, and Edge.
Basic Implementation
Let's start with a simple SSE client implementation:
// Create an EventSource instance pointing to the SSE endpoint
const eventSource = new EventSource('/api/events');
// Listen for messages from the server
eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Received data:', data);
  // Update your UI with the received data
};
// Handle connection open
eventSource.onopen = () => {
  console.log('SSE connection established');
};
// Handle errors
eventSource.onerror = (error) => {
  console.error('SSE connection error:', error);
  // Automatic reconnection will happen by default
};
// To close the connection when no longer needed
function closeConnection() {
  eventSource.close();
}Handling Different Event Types
One powerful feature of SSE is the ability to send different types of events over the same connection:
const eventSource = new EventSource('/api/events');
// Listen for specific event types
eventSource.addEventListener('update', (event) => {
  const data = JSON.parse(event.data);
  console.log('Received update:', data);
  // Handle updates
});
eventSource.addEventListener('notification', (event) => {
  const data = JSON.parse(event.data);
  console.log('Received notification:', data);
  // Handle notifications
});
// The onmessage handler only catches events without an event type
eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Received default message:', data);
};Adding Authentication
For authenticated endpoints, you'll need to include credentials:
// With credentials for authenticated endpoints
const eventSource = new EventSource('/api/events', { withCredentials: true });If you need to send a token, you might need to customize the request further. Since EventSource doesn't support custom headers directly, you can pass the token in the URL:
const token = 'your-auth-token';
const eventSource = new EventSource(`/api/events?token=${token}`);Handling Reconnection Explicitly
While SSE has automatic reconnection, sometimes you want more control:
let eventSource;
let retryCount = 0;
const maxRetries = 5;
eventSource = new EventSource('/api/events');
eventSource.onopen = () => {
  console.log('Connection established');
  retryCount = 0; // Reset retry count on successful connection
};
eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Received:', data);
};
function handleError(error) {
  console.error('SSE error:', error);
  
  // Close current connection
  eventSource.close();
  
  // Try to reconnect with exponential backoff
  if (retryCount < maxRetries) {
    const timeout = Math.pow(2, retryCount) * 1000;
    retryCount++;
    console.log(`Reconnecting in ${timeout}ms...`);
    
    setTimeout(() => {
      eventSource = new EventSource('/api/events');
      
      // Re-attach event listeners for the new connection
      eventSource.onopen = () => {
        console.log('Connection established');
        retryCount = 0;
      };
      
      eventSource.onmessage = (event) => {
        const data = JSON.parse(event.data);
        console.log('Received:', data);
      };
      
      eventSource.onerror = handleError;
    }, timeout);
  } else {
    console.error('Max retries reached. Giving up.');
    // Show an error to the user
  }
}
eventSource.onerror = handleError;Using with Modern Frameworks
React
import { useState, useEffect } from 'react';
function EventComponent() {
  const [events, setEvents] = useState([]);
  
  useEffect(() => {
    const eventSource = new EventSource('/api/events');
    
    eventSource.onmessage = (event) => {
      const newEvent = JSON.parse(event.data);
      setEvents(prevEvents => [newEvent, ...prevEvents].slice(0, 10));
    };
    
    // Cleanup function
    return () => {
      eventSource.close();
    };
  }, []); // Empty dependency array means this runs once on mount
  
  return (
    <div>
      <h2>Latest Events</h2>
      <ul>
        {events.map((event, index) => (
          <li key={index}>{event.message}</li>
        ))}
      </ul>
    </div>
  );
}Vue 3
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const events = ref([]);
let eventSource = null;
onMounted(() => {
  eventSource = new EventSource('/api/events');
  
  eventSource.onmessage = (event) => {
    const newEvent = JSON.parse(event.data);
    events.value = [newEvent, ...events.value].slice(0, 10);
  };
});
onUnmounted(() => {
  if (eventSource) {
    eventSource.close();
  }
});
</script>
<template>
  <div>
    <h2>Latest Events</h2>
    <ul>
      <li v-for="(event, index) in events" :key="index">
        {{ event.message }}
      </li>
    </ul>
  </div>
</template>Common Pitfalls and Solutions
1. Maximum Connections Limit
Browsers limit the number of concurrent SSE connections to the same domain (typically 6). Solutions:
- Use a single SSE connection and multiplex different events over it
 - Use domain sharding (different subdomains) for multiple connections
 - Consider WebSockets if you need many concurrent connections
 
2. Memory Leaks
SSE connections can cause memory leaks if not properly closed:
// Always close the connection when the component is unmounted
function cleanupSSE() {
  if (eventSource) {
    eventSource.close();
    eventSource = null;
  }
}3. Long Idle Connections
Some proxies or load balancers might close idle connections. Server should send keepalive messages:
// On the client, prepare to handle keep-alive events
eventSource.addEventListener('keepalive', (event) => {
  // Just ignore these events, they're just to keep the connection open
  console.log('Received keepalive');
});Performance Considerations
To optimize SSE performance:
- Keep messages small - Send only what's needed
 - Batch updates - For high-frequency updates, batch them on the server
 - Use compression - Enable gzip for SSE endpoints
 - Implement backoff for reconnections - As shown in the reconnection example
 - Set appropriate cache headers - To prevent caching of the event stream:
 
Cache-Control: no-cache
Connection: keep-aliveConclusion
Server-Sent Events provide a straightforward way to implement real-time updates in web applications. They're perfect for many use cases where you need server-to-client communication without the complexity of WebSockets.
By understanding the patterns and best practices shown in this tutorial, you can create robust applications that respond instantly to backend changes, providing a more dynamic and engaging user experience.
If you have any questions or run into issues implementing SSE in your frontend application, leave a comment below and I'll do my best to help!