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 namemessage- Message body (plain text or Markdown)title- Optional message titlepriority- Priority level (1-5)tags- Array of tagstimestamp- 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 <= 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
| 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:
- Check authentication token (for private topics)
- Verify topic exists
- Check network/firewall settings
- Review browser console for errors
No Messages Received
Problem: Connected but not receiving messages.
Solutions:
- Publish test message to verify topic
- Check
since_idparameter (might be too recent) - Verify network isn't buffering responses
- Use
-Nflag with curl (disable buffering)
High Memory Usage
Problem: Browser tab consuming too much memory.
Solutions:
- Limit displayed messages (keep last 50-100)
- Close connection when tab is hidden
- 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. 📡