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¶
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:
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¶
Message Format¶
Event Structure¶
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¶
- WebSocket Subscription - Bidirectional alternative
- Web App Guide - Subscribe via web interface
- Mobile App Guide - Subscribe on iOS/Android
- Publishing Guide - Send messages to topics
Pro Tip: Use SSE for most real-time subscriptions - it's simple, reliable, and works everywhere! Only use WebSocket if you need bidirectional communication. 📡