Developing Server-Sent Events (SSE) for Web Apps

When updating the order status, the client has to reload the page or poll the API every 5 seconds. This increases server load and degrades UX. A similar situation occurs on a financial platform where quotes change every second—WebSocket is overkill, but constant polling kills the database. We solve

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1281
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1237
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    977
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1026
  • image_website-sbh_0.webp
    Website development for SBH Partners
    1103
  • image_website-_0.webp
    Website development for Red Pear
    550

When updating the order status, the client has to reload the page or poll the API every 5 seconds. This increases server load and degrades UX. A similar situation occurs on a financial platform where quotes change every second—WebSocket is overkill, but constant polling kills the database. We solve this problem with Server-Sent Events (SSE)—a one-way streaming technology from server to client over HTTP. Our experience shows: SSE reduces the number of requests by 10x and delivers events in <1 second.

Why SSE and not WebSocket?

Criterion SSE WebSocket
Direction One-way (server→client) Bidirectional
HTTP compatibility Native HTTP/1.1 Requires upgrade
Auto-reconnection Built-in Needs implementation
Proxy compatibility Works seamlessly Often blocked
CORS GET without preflight Preflight for non-GET

SSE is better suited for tasks where notifications and live updates matter. Client-to-server data sending is rare. Setting up SSE takes on average half the time compared to WebSocket. For example, for order status notifications SSE is ideal, while for chat WebSocket is better.

How does SSE scale across multiple servers?

In one project for a large e-commerce, we implemented SSE with distributed delivery via Redis Pub/Sub. The client achieved a reduction in order status update time from 5 minutes to 1 second. The system handles 10,000 concurrent connections without event loss. Scaling is achieved through a message broker: all servers subscribe to a common channel and publish events to all clients. We have used this pattern in over 30 projects in recent years.

Technical Implementation

SSE Format

Server response is text/event-stream with fields data:, event:, id:, retry::

HTTP/1.1 200 OK Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive data: {"type":"notification","message":"New order"} event: order_update data: {"orderId":"123","status":"shipped"} id: msg_456 retry: 3000 : comment (ignored by client) 

Server endpoint (Node.js + Express)

Full server endpoint code with heartbeat and client registry
app.get('/api/events', (req, res) => { const userId = req.user.id; res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', // for Nginx: disable buffering }); // Immediately send the first packet (bypass nginx buffering) res.write(':ok\n\n'); // Add client to registry const clientId = nanoid(); clients.set(clientId, { res, userId }); // Heartbeat every 15 seconds const heartbeat = setInterval(() => { res.write(': ping\n\n'); }, 15000); req.on('close', () => { clearInterval(heartbeat); clients.delete(clientId); }); }); // Send event to specific user function sendToUser(userId: string, event: string, data: object) { clients.forEach(({ res, userId: uid }) => { if (uid === userId) { res.write(`event: ${event}\n`); res.write(`data: ${JSON.stringify(data)}\n\n`); } }); } // Broadcast to all function broadcast(event: string, data: object) { clients.forEach(({ res }) => { res.write(`event: ${event}\n`); res.write(`data: ${JSON.stringify(data)}\n\n`); }); } 

Client using EventSource

Connection via EventSource with named event support:

const eventSource = new EventSource('/api/events', { withCredentials: true }); // Default events (no event name) eventSource.onmessage = (e) => { const data = JSON.parse(e.data); console.log('Message:', data); }; // Named events eventSource.addEventListener('order_update', (e) => { const order = JSON.parse(e.data); updateOrderStatus(order.orderId, order.status); }); eventSource.addEventListener('notification', (e) => { showNotification(JSON.parse(e.data).message); }); // Error handling eventSource.onerror = (e) => { if (eventSource.readyState === EventSource.CLOSED) { console.log('Connection closed, automatically reconnecting...'); } }; 

For more details about EventSource, refer to the official documentation: MDN Web Docs.

Scaling with Redis Pub/Sub

sub.subscribe('user:events', (message) => { const { userId, event, data } = JSON.parse(message); sendToUser(userId, event, data); }); // From any service await pub.publish('user:events', JSON.stringify({ userId: 'user_123', event: 'payment_completed', data: { amount: 5000 } })); 

Example named events

Event Data Format
notification {"type":"success","message":"..."}
order_update {"orderId":"123","status":"shipped"}
payment_completed {"amount":5000,"currency":"USD"}

Real case: SSE for a financial platform

For a startup providing real-time cryptocurrency data, we designed an SSE-based system. The requirement was to send quotes for 50+ currency pairs to thousands of users without delay and without burdening client databases. Solution: each user subscribes to desired pairs via a custom endpoint, and servers aggregate events via Redis Pub/Sub. Result: delivery latency under 200 ms even with 10,000 connections. This allowed us to abandon WebSocket, simplify infrastructure, and save on development. We guarantee stability of this scheme under high load.

How to ensure reliable reconnection?

SSE automatically reconnects, but it is important to configure a heartbeat. If the server does not send data for 30 seconds, the browser may close the connection. Therefore, we send an empty comment (: ping) every 15 seconds. This keeps the connection alive. Also, use the header X-Accel-Buffering: no for Nginx to prevent data buffering.

SSE limitations

  • Server→client only — the client cannot send data via SSE (only new EventSource requests or separate API calls).
  • Connection limit in HTTP/1.1 — browsers limit to 6 connections per domain. SSE consumes one. Solution: HTTP/2 (single multiplexed stream).
  • No IE support — use a polyfill eventsource for older browsers.

What's included and typical mistakes

  • Designing the event schema and data types.
  • Implementing the server endpoint with authentication and heartbeat.
  • Client integration (React hook, Angular service, or native EventSource).
  • Load testing up to 10,000 connections.
  • Documenting the protocol and code.
  • Training the client's team.

Typical mistakes when implementing SSE:

  • Forgetting Nginx buffering — missing the X-Accel-Buffering: no header.
  • Using SSE for bidirectional communication — WebSocket is more appropriate.
  • Not setting a heartbeat — the connection may drop and not recover.

Timelines

SSE endpoint with authentication, heartbeat, Redis scaling: 3–5 days. With typed events, client-side hook (useSSE), and notification integration: 1–2 weeks.

Let's evaluate your project — contact us for a consultation. Over the years, we have implemented many SSE-based solutions, guaranteeing stability under high load. If you need to implement live updates, get a consultation today.