Skip to main 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": 1,
"tags": ["critical", "server"],
"timestamp": "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
  • timestamp - 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​

CodeMeaningAction
200SuccessConnection established
401UnauthorizedCheck authentication token
404Topic not foundVerify topic name
403ForbiddenCheck topic access permissions
429Rate limitedWait 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 <= 2) {
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.timestamp).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['timestamp'].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=2)

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​

FeatureSSEWebSocket
DirectionServer → Client onlyBidirectional
ProtocolHTTPWS/WSS
ReconnectionAutomaticManual
Use caseReal-time updatesChat, gaming
ComplexitySimpleMore complex
Browser supportAll modern browsersAll 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. 📡