# How to Fix SSE Disconnects Every 30 Seconds

# 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:**

```javascript
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:**

```python
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
<?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

```javascript
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:**

```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:**

```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):**

```hcl
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:**

```javascript
const server = app.listen(3000);
server.keepAliveTimeout = 65000; // 65 seconds
server.headersTimeout = 66000;   // 66 seconds
```

**PHP (php.ini):**

```ini
max_execution_time = 0
default_socket_timeout = 0
```

## Tips

**1. Monitor Connection Health**
Add logging to track disconnects and reconnects:

```javascript
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:

```javascript
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:

1. **Send heartbeats every 25 seconds** from the server
2. **Set proper SSE headers** (Content-Type, Cache-Control, X-Accel-Buffering)
3. **Configure proxy timeouts** to be longer than your heartbeat interval
4. **Implement exponential backoff** on the client
5. **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.
