Skip to content

Server-Sent Events (SSE)

Subscribe to Notifer topics in real-time using Server-Sent Events (SSE).

What is SSE?

Server-Sent Events (SSE) is a technology that allows a server to push data to a client over HTTP. It's perfect for real-time notifications because:

  • Simple: Works over standard HTTP (no special protocols)
  • Reliable: Automatic reconnection on connection loss
  • Efficient: Long-lived connection with low overhead
  • Universal: Works in all modern browsers and curl
  • Firewall-friendly: Uses standard HTTP/HTTPS ports

Example flow:

Client → Server: GET /my-topic/sse
Server → Client: (connection stays open)
Server → Client: event: message\ndata: {"message": "Hello"}\n\n
Server → Client: event: message\ndata: {"message": "World"}\n\n

Quick Start

Using cURL

curl -N https://app.notifer.io/my-topic/sse

Output:

event: message
data: {"id": "uuid", "message": "First message", "priority": 3, "timestamp": "2025-11-22T10:00:00Z"}

event: message
data: {"id": "uuid", "message": "Second message", "priority": 5, "timestamp": "2025-11-22T10:01:00Z"}

Using JavaScript (Browser)

const eventSource = new EventSource('https://app.notifer.io/my-topic/sse');

eventSource.onmessage = (event) => {
  const message = JSON.parse(event.data);
  console.log('New message:', message);
};

eventSource.onerror = (error) => {
  console.error('SSE error:', error);
};

// Close connection when done
// eventSource.close();

Using Python

import requests

url = 'https://app.notifer.io/my-topic/sse'

with requests.get(url, stream=True) as response:
    for line in response.iter_lines():
        if line:
            decoded_line = line.decode('utf-8')
            if decoded_line.startswith('data:'):
                message_json = decoded_line[5:].strip()
                print(message_json)

Subscribing to Private Topics

Using Authentication Token

For private topics, include your JWT token or topic access token:

# Method 1: Query parameter
curl -N "https://app.notifer.io/my-private-topic/sse?token=YOUR_JWT_TOKEN"

# Method 2: Authorization header
curl -N https://app.notifer.io/my-private-topic/sse \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

JavaScript:

// Include token in URL
const eventSource = new EventSource(
  'https://app.notifer.io/my-private-topic/sse?token=' + encodeURIComponent(token)
);

Python:

headers = {'Authorization': f'Bearer {token}'}
with requests.get(url, headers=headers, stream=True) as response:
    # ... process events

Query Parameters

since_id - Resume from Last Message

Get all messages since a specific message ID:

curl -N "https://app.notifer.io/my-topic/sse?since_id=LAST_MESSAGE_ID"

Use case: Reconnect and catch up on missed messages.

Example:

let lastMessageId = null;

function connect() {
  const url = lastMessageId
    ? `https://app.notifer.io/my-topic/sse?since_id=${lastMessageId}`
    : 'https://app.notifer.io/my-topic/sse';

  const eventSource = new EventSource(url);

  eventSource.onmessage = (event) => {
    const message = JSON.parse(event.data);
    lastMessageId = message.id; // Save for reconnection
    displayMessage(message);
  };

  eventSource.onerror = () => {
    eventSource.close();
    setTimeout(connect, 5000); // Reconnect after 5s
  };
}

connect();

token - Authentication

curl -N "https://app.notifer.io/my-topic/sse?token=YOUR_TOKEN"

Message Format

Event Structure

event: message
data: {...json...}

Fields: - event: - Event type (always "message" for notifications) - data: - JSON message data - Empty line - Marks end of event

Message Data

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "topic": "my-topic",
  "message": "Server is down!",
  "title": "Critical Alert",
  "priority": 5,
  "tags": ["critical", "server"],
  "created_at": "2025-11-22T10:30:00Z"
}

Fields: - id - Unique message ID (UUID) - topic - Topic name - message - Message body (plain text or Markdown) - title - Optional message title - priority - Priority level (1-5) - tags - Array of tags - created_at - Timestamp (ISO 8601)

Reconnection Handling

SSE connections automatically reconnect when dropped, but you should handle reconnections gracefully:

Browser (EventSource)

function createConnection() {
  const eventSource = new EventSource('https://app.notifer.io/my-topic/sse');

  eventSource.onopen = () => {
    console.log('✅ Connected');
  };

  eventSource.onerror = (error) => {
    console.error('❌ Connection error:', error);
    // Browser will automatically reconnect
    // EventSource has built-in retry logic
  };

  eventSource.onmessage = (event) => {
    const message = JSON.parse(event.data);
    handleMessage(message);
  };

  return eventSource;
}

let connection = createConnection();

// Manual reconnect if needed
function reconnect() {
  connection.close();
  connection = createConnection();
}

Python (Manual Reconnection)

import requests
import time
import json

def subscribe(topic, token=None):
    url = f'https://app.notifer.io/{topic}/sse'
    headers = {}
    if token:
        headers['Authorization'] = f'Bearer {token}'

    while True:
        try:
            print('🔌 Connecting...')
            with requests.get(url, headers=headers, stream=True) as response:
                response.raise_for_status()
                print('✅ Connected')

                for line in response.iter_lines():
                    if line:
                        decoded = line.decode('utf-8')
                        if decoded.startswith('data:'):
                            message_json = decoded[5:].strip()
                            message = json.loads(message_json)
                            handle_message(message)

        except requests.exceptions.RequestException as e:
            print(f'❌ Connection error: {e}')
            print('🔄 Reconnecting in 5 seconds...')
            time.sleep(5)

def handle_message(message):
    print(f"📬 [{message['priority']}] {message['title']}: {message['message']}")

subscribe('my-topic')

Error Handling

HTTP Status Codes

Code Meaning Action
200 Success Connection established
401 Unauthorized Check authentication token
404 Topic not found Verify topic name
403 Forbidden Check topic access permissions
429 Rate limited Wait and retry with backoff

Handling Errors in JavaScript

const eventSource = new EventSource('https://app.notifer.io/my-topic/sse');

eventSource.addEventListener('error', (event) => {
  if (event.target.readyState === EventSource.CLOSED) {
    console.error('Connection closed by server');
  } else if (event.target.readyState === EventSource.CONNECTING) {
    console.log('Reconnecting...');
  }
});

Real-World Examples

Server Monitoring Dashboard

const topics = ['prod-web', 'prod-api', 'prod-db'];
const connections = {};

topics.forEach(topic => {
  const eventSource = new EventSource(`https://app.notifer.io/${topic}/sse`);

  eventSource.onmessage = (event) => {
    const message = JSON.parse(event.data);
    updateDashboard(topic, message);

    // Show alert for critical messages
    if (message.priority >= 4) {
      showAlert(message);
      playAlertSound();
    }
  };

  connections[topic] = eventSource;
});

function updateDashboard(topic, message) {
  const container = document.getElementById(`topic-${topic}`);
  const messageEl = document.createElement('div');
  messageEl.className = `message priority-${message.priority}`;
  messageEl.innerHTML = `
    <strong>${message.title}</strong>
    <p>${message.message}</p>
    <small>${new Date(message.created_at).toLocaleString()}</small>
  `;
  container.prepend(messageEl);
}

Python Alert Monitor

import requests
import json
from datetime import datetime

def monitor_alerts(topic, priority_threshold=3):
    """Monitor topic and print only important alerts"""
    url = f'https://app.notifer.io/{topic}/sse'

    print(f'📡 Monitoring {topic} (priority >= {priority_threshold})')

    with requests.get(url, stream=True) as response:
        for line in response.iter_lines():
            if line and line.startswith(b'data:'):
                data = line[5:].strip()
                message = json.loads(data)

                if message['priority'] >= priority_threshold:
                    timestamp = datetime.fromisoformat(message['created_at'].replace('Z', '+00:00'))
                    print(f"\n🚨 [{message['priority']}] {message['title']}")
                    print(f"   {message['message']}")
                    print(f"   Tags: {', '.join(message['tags'])}")
                    print(f"   Time: {timestamp.strftime('%Y-%m-%d %H:%M:%S')}")

monitor_alerts('production-alerts', priority_threshold=4)

Multi-Topic Subscription (Python)

import requests
import json
from threading import Thread

def subscribe_topic(topic, callback):
    """Subscribe to a topic in a separate thread"""
    url = f'https://app.notifer.io/{topic}/sse'

    while True:
        try:
            with requests.get(url, stream=True) as response:
                for line in response.iter_lines():
                    if line and line.startswith(b'data:'):
                        message = json.loads(line[5:].strip())
                        callback(topic, message)
        except Exception as e:
            print(f'Error in {topic}: {e}')
            time.sleep(5)

def handle_message(topic, message):
    print(f"[{topic}] {message['title']}: {message['message']}")

# Subscribe to multiple topics in parallel
topics = ['deployments', 'errors', 'monitoring']

threads = []
for topic in topics:
    thread = Thread(target=subscribe_topic, args=(topic, handle_message))
    thread.daemon = True
    thread.start()
    threads.append(thread)

# Keep main thread alive
for thread in threads:
    thread.join()

SSE vs WebSocket

Feature SSE WebSocket
Direction Server → Client only Bidirectional
Protocol HTTP WS/WSS
Reconnection Automatic Manual
Use case Real-time updates Chat, gaming
Complexity Simple More complex
Browser support All modern browsers All modern browsers

When to use SSE: - ✅ One-way notifications (server → client) - ✅ Simple setup and debugging - ✅ Automatic reconnection needed - ✅ HTTP-only environments

When to use WebSocket: - ✅ Bidirectional communication needed - ✅ Very high frequency updates - ✅ Gaming or chat applications

For Notifer, SSE is recommended because: - Notifications are one-way (server → client) - Automatic reconnection simplifies client code - Works with standard HTTP infrastructure

Browser Support

SSE is supported in all modern browsers:

  • ✅ Chrome/Edge 6+
  • ✅ Firefox 6+
  • ✅ Safari 5+
  • ✅ Opera 11+
  • ❌ Internet Explorer (not supported)

Polyfills: For IE support, use EventSource polyfill

Best Practices

1. Save Last Message ID

Always track the last message ID for reconnection:

let lastId = localStorage.getItem('lastMessageId');

const url = lastId
  ? `https://app.notifer.io/topic/sse?since_id=${lastId}`
  : 'https://app.notifer.io/topic/sse';

const eventSource = new EventSource(url);

eventSource.onmessage = (event) => {
  const message = JSON.parse(event.data);
  lastId = message.id;
  localStorage.setItem('lastMessageId', lastId);
};

2. Handle Visibility Changes

Pause/resume connection when tab is hidden:

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    // Optionally close connection when tab is hidden
    // eventSource.close();
  } else {
    // Reconnect when tab is visible
    // eventSource = createConnection();
  }
});

3. Implement Heartbeat Detection

Detect stale connections:

let heartbeatTimeout;

eventSource.onmessage = (event) => {
  clearTimeout(heartbeatTimeout);

  // Expect message within 60 seconds
  heartbeatTimeout = setTimeout(() => {
    console.warn('No heartbeat received, reconnecting...');
    eventSource.close();
    eventSource = createConnection();
  }, 60000);
};

4. Use Exponential Backoff

For manual reconnections:

import time

def exponential_backoff(attempt, max_wait=60):
    """Calculate backoff delay with exponential growth"""
    wait = min(2 ** attempt, max_wait)
    return wait

attempt = 0
while True:
    try:
        subscribe(topic)
        attempt = 0  # Reset on success
    except Exception as e:
        wait = exponential_backoff(attempt)
        print(f'Reconnecting in {wait}s...')
        time.sleep(wait)
        attempt += 1

Limitations

  • Message size: Max 4,000 characters per message
  • Connection limit: 100 concurrent SSE connections per IP (Free tier)
  • Message history: Last 100 messages cached (12 hour TTL)
  • Browser limit: 6 concurrent SSE connections per domain (browser limitation)

Troubleshooting

Connection Immediately Closes

Problem: SSE connection opens and immediately closes.

Solutions: 1. Check authentication token (for private topics) 2. Verify topic exists 3. Check network/firewall settings 4. Review browser console for errors

No Messages Received

Problem: Connected but not receiving messages.

Solutions: 1. Publish test message to verify topic 2. Check since_id parameter (might be too recent) 3. Verify network isn't buffering responses 4. Use -N flag with curl (disable buffering)

High Memory Usage

Problem: Browser tab consuming too much memory.

Solutions: 1. Limit displayed messages (keep last 50-100) 2. Close connection when tab is hidden 3. Clear old messages from DOM periodically

Next Steps


Pro Tip: Use SSE for most real-time subscriptions - it's simple, reliable, and works everywhere! Only use WebSocket if you need bidirectional communication. 📡