Skip to main content

Command Palette

Search for a command to run...

How to Fix SSE Disconnects Every 30 Seconds

Learn: How to Fix SSE Disconnects Every 30 Seconds

Updated
5 min readView as Markdown
T

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:

  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.