How to Fix SSE Disconnects Every 30 Seconds
Learn: How to Fix SSE Disconnects Every 30 Seconds
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
How to Fix SSE Disconnects Every 30 Seconds
Problem
Server-Sent Events (SSE) connections are dropping every 30 seconds like clockwork. Your real-time data stream stutters, reconnection logic fires repeatedly, and users see loading spinners instead of smooth updates. This is one of the most frustrating issues when implementing SSE, and it's almost always preventable.
Cause
The 30-second disconnect pattern is rarely random. Here are the actual culprits:
1. Proxy/Load Balancer Timeout Most reverse proxies (nginx, Apache, AWS ALB) have default idle timeouts between 30-60 seconds. If no data flows through the connection, they assume it's dead and close it.
2. Firewall Inactivity Rules Corporate firewalls and ISPs often terminate idle connections to reclaim resources. A silent SSE stream looks idle to them.
3. Browser Inactivity Timeout Some browsers (especially older versions) timeout idle connections. This is less common but still happens.
4. Missing Keep-Alive Mechanism Your server isn't sending periodic heartbeats, so the connection appears dormant to intermediaries.
5. Incorrect Content-Type or Headers Missing or wrong headers prevent proper SSE negotiation, causing proxies to mishandle the stream.
6. Server-Side Resource Limits PHP, Node.js, or other runtimes have default timeouts that kill long-lived connections.
Solution
Step 1: Implement Server-Side Keep-Alive
The most reliable fix is sending periodic heartbeat comments. SSE comments (lines starting with :) are ignored by clients but keep the connection alive.
Node.js/Express:
const express = require('express');
const app = express();
app.get('/events', (req, res) => {
// Set proper headers
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
// Send initial comment to establish connection
res.write(': connected\n\n');
// Send heartbeat every 25 seconds (below 30s threshold)
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n');
}, 25000);
// Send actual data when available
const dataInterval = setInterval(() => {
const data = {
timestamp: new Date().toISOString(),
value: Math.random()
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 5000);
// Cleanup on disconnect
req.on('close', () => {
clearInterval(heartbeat);
clearInterval(dataInterval);
res.end();
});
});
app.listen(3000, () => console.log('SSE server running on :3000'));
Python/Flask:
from flask import Flask, Response
from datetime import datetime
import time
import threading
app = Flask(__name__)
@app.route('/events')
def events():
def generate():
# Send initial connection marker
yield ': connected\n\n'
# Heartbeat counter
heartbeat_count = 0
try:
while True:
# Send heartbeat every 25 seconds
if heartbeat_count % 5 == 0:
yield ': heartbeat\n\n'
# Send actual data
data = {
'timestamp': datetime.now().isoformat(),
'value': heartbeat_count
}
yield f'data: {data}\n\n'
heartbeat_count += 1
time.sleep(5)
except GeneratorExit:
pass
return Response(
generate(),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no',
'Connection': 'keep-alive'
}
)
if __name__ == '__main__':
app.run(debug=True)
PHP:
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
// Disable output buffering
if (ob_get_level() == 0) ob_start();
// Send initial marker
echo ": connected\n\n";
flush();
$heartbeat = 0;
while (true) {
// Send heartbeat every 25 seconds
if ($heartbeat % 5 == 0) {
echo ": heartbeat\n\n";
flush();
}
// Send actual data
$data = json_encode([
'timestamp' => date('c'),
'value' => $heartbeat
]);
echo "data: $data\n\n";
flush();
$heartbeat++;
sleep(5);
}
?>
Step 2: Configure Client-Side Reconnection
class SSEClient {
constructor(url, options = {}) {
this.url = url;
this.eventSource = null;
this.reconnectInterval = options.reconnectInterval || 3000;
this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
this.reconnectAttempts = 0;
this.listeners = {};
}
connect() {
try {
this.eventSource = new EventSource(this.url);
this.eventSource.onopen = () => {
console.log('SSE connected');
this.reconnectAttempts = 0;
};
this.eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
this.emit('message', data);
};
this.eventSource.onerror = (error) => {
console.error('SSE error:', error);
this.handleDisconnect();
};
// Listen for custom events
this.eventSource.addEventListener('custom-event', (event) => {
this.emit('custom-event', JSON.parse(event.data));
});
} catch (error) {
console.error('Failed to create EventSource:', error);
this.handleDisconnect();
}
}
handleDisconnect() {
if (this.eventSource) {
this.eventSource.close();
}
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => this.connect(), delay);
} else {
console.error('Max reconnection attempts reached');
this.emit('failed');
}
}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
disconnect() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
}
// Usage
const client = new SSEClient('/events', {
reconnectInterval: 3000,
maxReconnectAttempts: 10
});
client.on('message', (data) => {
console.log('Received:', data);
});
client.on('failed', () => {
console.log('Connection permanently failed');
});
client.connect();
Step 3: Configure Proxy/Load Balancer
Nginx:
location /events {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 60s;
}
Apache:
<Location /events>
ProxyPass http://backend/events
ProxyPassReverse http://backend/events
SetEnv proxy-nokeepalive 1
SetEnv proxy-initial-connection-upgrade 1
TimeOut 3600
</Location>
AWS ALB (via Terraform):
resource "aws_lb_target_group" "sse" {
name = "sse-targets"
port = 3000
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
healthy_threshold = 2
unhealthy_threshold = 2
timeout = 5
interval = 30
path = "/health"
matcher = "200"
}
stickiness {
type = "lb_cookie"
enabled = true
cookie_duration = 86400
}
deregistration_delay = 30
}
Step 4: Set Appropriate Server Timeouts
Node.js:
const server = app.listen(3000);
server.keepAliveTimeout = 65000; // 65 seconds
server.headersTimeout = 66000; // 66 seconds
PHP (php.ini):
max_execution_time = 0
default_socket_timeout = 0
Tips
1. Monitor Connection Health Add logging to track disconnects and reconnects:
client.on('message', (data) => {
if (data.type === 'heartbeat') {
console.log(`[${new Date().toISOString()}] Heartbeat received`);
}
});
2. Use Exponential Backoff The client code above implements exponential backoff to avoid hammering the server during outages.
3. Test with Network Throttling Use Chrome DevTools to simulate slow/offline conditions and verify reconnection logic works.
4. Set Heartbeat Below Proxy Timeout If your proxy timeout is 60s, send heartbeats every 25-30s maximum.
5. Monitor Server Resource Usage
Long-lived connections consume memory. Use tools like top or New Relic to track impact.
6. Implement Graceful Degradation Fall back to polling if SSE fails after max reconnection attempts:
client.on('failed', () => {
console.log('Switching to polling');
startPolling();
});
7. Use Connection Pooling For high-traffic scenarios, consider connection limits and implement queuing.
Takeaway
The 30-second disconnect is almost always caused by missing keep-alive heartbeats combined with proxy timeouts. Fix it by:
- Send heartbeats every 25 seconds from the server
- Set proper SSE headers (Content-Type, Cache-Control, X-Accel-Buffering)
- Configure proxy timeouts to be longer than your heartbeat interval
- Implement exponential backoff on the client
- Test thoroughly with network throttling enabled
This combination eliminates 99% of SSE timeout issues. The key insight: proxies and firewalls don't understand "idle" SSE connections—they need periodic data flow to stay alive.