Server-Sent Events

technology

Server-Sent Events

Definition

Server-Sent Events (SSE) is a W3C standardized web technology that enables servers to push real-time updates to clients over a persistent HTTP connection. Unlike WebSockets, which establish a full-duplex bidirectional channel, SSE is unidirectional: data flows exclusively from server to client. The protocol is built on top of standard HTTP, using the text/event-stream content type and a simple text-based wire format, making it compatible with existing HTTP infrastructure including proxies, load balancers, CDNs, and firewalls without special configuration.

The SSE specification defines the EventSource browser API, which provides automatic reconnection with exponential backoff, event ID tracking for seamless resumption after disconnection, and named event types for multiplexing different data streams over a single connection. These built-in capabilities eliminate significant client-side complexity that developers would otherwise need to implement manually with WebSockets. The simplicity of the protocol -- it is essentially a long-lived HTTP response with a structured text body -- makes it remarkably robust and easy to debug, as standard HTTP tools (curl, browser developer tools, HTTP proxies) work without modification.

SSE occupies a specific niche in the real-time communication spectrum: it is the optimal choice when the primary data flow is server-to-client, when HTTP compatibility is important, and when the simplicity of implementation and debugging outweighs the need for client-to-server streaming. Common use cases include live feeds, progress notifications, dashboard updates, log tailing, and any scenario where the server is the primary source of new information.

Protocol Specification

The SSE wire format is deliberately simple, consisting of UTF-8 text lines with field-name/value pairs separated by colons.

Wire Format

event: security-rating\n
id: evt-2026-0214-001\n
retry: 5000\n
data: {"domain":"example.com","grade":"B","score":780}\n
\n
event: asset-discovered\n
id: evt-2026-0214-002\n
data: {"type":"subdomain","value":"api.example.com"}\n
\n
: this is a comment, used as a keepalive\n
\n

Each message is terminated by a blank line (\n\n). The protocol defines four field types:

FieldPurposeExample
dataEvent payload (required)data: {"score": 780}
eventNamed event type for client-side dispatchevent: rating-update
idEvent identifier for reconnection resumptionid: evt-001
retryClient reconnection delay in millisecondsretry: 5000

Multi-line data is supported by repeating the data field. Comment lines (prefixed with :) serve as keepalives to prevent proxy timeout disconnections.

HTTP Headers

The server response uses specific headers to establish the SSE connection:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no

The X-Accel-Buffering: no header is important for Nginx reverse proxies, preventing response buffering that would delay event delivery. Similarly, Cache-Control: no-cache ensures intermediaries do not cache the event stream.

EventSource Browser API

The client-side EventSource API provides a high-level interface for consuming SSE streams with built-in reliability features.

// Basic SSE consumption
const source = new EventSource('/api/v1/stream/security-events');

// Default message handler
source.onmessage = (event) => {
  const data = JSON.parse(event.data);
  updateDashboard(data);
};

// Named event handlers
source.addEventListener('rating-update', (event) => {
  const rating = JSON.parse(event.data);
  updateSecurityRating(rating.domain, rating.grade, rating.score);
});

source.addEventListener('asset-discovered', (event) => {
  const asset = JSON.parse(event.data);
  addAssetToInventory(asset);
});

// Connection lifecycle
source.onopen = () => console.log('SSE connection established');
source.onerror = (event) => {
  if (source.readyState === EventSource.CONNECTING) {
    console.log('Reconnecting...');
  }
};

Automatic Reconnection

The EventSource API automatically reconnects when the connection drops, using the retry interval specified by the server (default 3 seconds). On reconnection, it sends a Last-Event-ID header containing the id of the last received event, enabling the server to resume the stream from where it left off.

Initial connection:
  Client → GET /stream HTTP/1.1
  Server → 200 OK (begins streaming)
  Server → id: 42\ndata: event-42\n\n
  Server → id: 43\ndata: event-43\n\n
  [connection drops]

Reconnection (automatic):
  Client → GET /stream HTTP/1.1
           Last-Event-ID: 43
  Server → 200 OK (resumes from event 44)
  Server → id: 44\ndata: event-44\n\n

This built-in resumption mechanism is one of SSE's strongest advantages over raw WebSockets, where reconnection and state recovery must be implemented entirely by the application.

SSE vs WebSocket Comparison

The choice between SSE and WebSockets depends on the communication pattern, infrastructure constraints, and complexity budget.

DimensionServer-Sent EventsWebSocket
DirectionServer to client onlyBidirectional
ProtocolHTTP (text/event-stream)WebSocket protocol (ws://)
TransportHTTP/1.1 or HTTP/2TCP with HTTP upgrade
Data formatUTF-8 text onlyText or binary frames
Auto-reconnectBuilt-in with Last-Event-IDManual implementation required
Proxy compatibilityExcellent (standard HTTP)Variable (requires upgrade support)
Browser supportAll modern browsersAll modern browsers
Max connections6 per domain (HTTP/1.1), unlimited (HTTP/2)Unlimited
CompressionHTTP compression (gzip, br)Per-message deflate extension
AuthenticationStandard HTTP headers, cookiesCustom (often token in URL or first message)
DebuggingStandard HTTP toolsSpecialized WebSocket inspectors
ComplexityLowModerate to high

When to Choose SSE

  • Server is the primary data source (dashboards, feeds, notifications)
  • HTTP infrastructure must be preserved (corporate proxies, CDNs)
  • Automatic reconnection with state resumption is needed
  • Simplicity of implementation and debugging is valued
  • HTTP/2 is available (eliminates connection limit)

When to Choose WebSocket

  • Bidirectional communication required (chat, collaborative editing)
  • Binary data transfer needed (file uploads, media streaming)
  • Very high message frequency in both directions
  • Custom subprotocols required

SSE over HTTP/2

HTTP/2 significantly improves SSE's capabilities by multiplexing multiple streams over a single TCP connection. The HTTP/1.1 limitation of 6 connections per domain (shared across all SSE streams and regular requests) is eliminated entirely.

FeatureSSE + HTTP/1.1SSE + HTTP/2
Connections per domain6 (shared)1 (multiplexed)
Head-of-line blockingPer connectionPer stream (independent)
Header compressionNoneHPACK
Server pushNot availableAvailable
Concurrent streamsLimited by connectionsHundreds per connection

For the Prismatic Platform, HTTP/2 support means that a single browser tab displaying the Perimeter dashboard at /perimeter can simultaneously consume separate SSE streams for security ratings, asset discovery, compliance status, and quality metrics without exhausting connection limits.

Phoenix SSE Implementation

The Prismatic Platform implements SSE endpoints in the Phoenix framework using Plug's chunked response mechanism.

defmodule PrismaticApi.SSEController do
  use PrismaticApi, :controller

  def stream(conn, %{"topic" => topic}) do
    conn =
      conn
      |> put_resp_content_type("text/event-stream")
      |> put_resp_header("cache-control", "no-cache")
      |> put_resp_header("x-accel-buffering", "no")
      |> send_chunked(200)

    # Subscribe to PubSub topic
    Phoenix.PubSub.subscribe(PrismaticWeb.PubSub, topic)

    # Enter receive loop
    stream_loop(conn, topic)
  end

  defp stream_loop(conn, topic) do
    receive do
      {:event, event_type, data, id} ->
        case send_sse_event(conn, event_type, data, id) do
          {:ok, conn} -> stream_loop(conn, topic)
          {:error, _reason} -> conn  # Client disconnected
        end

      :keepalive ->
        case chunk(conn, ": keepalive\n\n") do
          {:ok, conn} -> stream_loop(conn, topic)
          {:error, _reason} -> conn
        end
    after
      30_000 ->
        # Send keepalive every 30 seconds
        case chunk(conn, ": keepalive\n\n") do
          {:ok, conn} -> stream_loop(conn, topic)
          {:error, _reason} -> conn
        end
    end
  end

  defp send_sse_event(conn, event_type, data, id) do
    payload =
      "event: #{event_type}\n" <>
      "id: #{id}\n" <>
      "data: #{Jason.encode!(data)}\n\n"

    chunk(conn, payload)
  end
end

This implementation leverages Phoenix's PubSub system as the bridge between backend stream processing pipelines and SSE delivery. When a Broadway pipeline processes a security event, it publishes to a PubSub topic. The SSE controller, subscribed to that topic, receives the event and pushes it to the client as an SSE message.

Use Cases in Prismatic

The Prismatic Platform uses SSE for several categories of real-time data delivery, primarily through the Prismatic API for external consumers.

Use CaseEvent TypeTypical FrequencyExample Payload
EASM scan progressscan-progressEvery 1-5 seconds{"domain":"example.com","progress":72,"found":15}
Security rating updatesrating-updatePer domain assessed{"domain":"example.com","grade":"B","score":780}
Asset discoveryasset-discoveredPer asset found{"type":"subdomain","value":"api.example.com"}
Compliance check resultscompliance-resultPer check completed{"framework":"nis2","control":"5.1","status":"pass"}
Quality telemetryquality-metricEvery 30 seconds{"domain":"compilation","score":100}
Long-running operation statusoperation-statusPer status change{"operation_id":"op-123","status":"analyzing"}

SSE is preferred over WebSockets for API consumers because it integrates naturally with the REST API paradigm: clients make a standard HTTP GET request and receive a streaming response. No protocol upgrade, no special client libraries, and standard HTTP authentication (Bearer tokens, cookies) works without modification.

SSE vs Phoenix LiveView

Within the Prismatic Platform, LiveView and SSE serve different audiences and use cases.

AspectLiveViewSSE
AudienceBrowser users (dashboards)API consumers (integrations)
TransportWebSocket (Phoenix Channel)HTTP (text/event-stream)
RenderingServer-rendered HTML diffsRaw JSON data
InteractivityFull bidirectional (clicks, forms)Unidirectional (server push only)
State managementServer-side (assigns)Client-side (consumer responsibility)
Client libraryPhoenix LiveView JSNative EventSource API

LiveView powers the interactive Perimeter dashboard at /perimeter where users click, filter, and interact with security data. SSE powers the API streaming endpoints at /api/v1/stream/* where automated systems consume real-time security events for integration into external SIEM, SOAR, or dashboarding tools.

Error Handling and Resilience

Robust SSE implementations must handle several failure modes gracefully.

Failure ModeServer BehaviorClient Behavior
Client disconnectDetect via chunk error, clean upAutomatic reconnect with Last-Event-ID
Server restartN/A (connection lost)Reconnect, resume from last ID
Proxy timeoutSend keepalive comments every 15-30sTransparent (keepalive prevents timeout)
Invalid event formatLog error, skip malformed eventonmessage not fired, connection preserved
Authentication expirySend 401 or close streamonerror fires, manual re-auth needed
Backpressure (slow client)Buffer up to limit, then drop oldestMay miss events (use IDs for gap detection)

The keepalive mechanism (comment lines sent at regular intervals) is particularly important in production environments where reverse proxies like Nginx impose idle connection timeouts. Without keepalives, an SSE connection with infrequent events would be terminated by the proxy, forcing unnecessary reconnections.

  • WebSocket - Bidirectional alternative for interactive communication patterns
  • Channel - Phoenix Channel abstraction over WebSocket transport
  • REST API - HTTP API pattern complemented by SSE for streaming responses
  • Phoenix - Framework supporting both SSE and WebSocket transports
  • LiveView - Server-rendered real-time UI using WebSocket (complementary to SSE)
  • PubSub - Internal message distribution feeding SSE streams from backend pipelines
  • Stream Processing - Backend pipeline producing events for SSE delivery
  • Backpressure - Flow control relevant to SSE when clients consume slowly
  • Load Balancing - Infrastructure concern for distributing long-lived SSE connections
  • Observability - Monitoring SSE connection health and event delivery metrics
  • Socket.IO

See Also

  • Technologies - Communication protocol options and technology stack
  • Architecture - Real-time data delivery patterns in the platform architecture
  • Apps - Applications implementing SSE endpoints for external consumers

Connect & Contribute

Created by Tomáš Korcak (korczis) | Open Source under GHL