ByteNoteByteNote

字节笔记本

2026年7月19日

Configuring Nginx for Server-Sent Events: A Practical Guide

API中转
¥120

Server-Sent Events (SSE) is a straightforward way to push real-time updates from server to client over standard HTTP. Unlike WebSocket, it doesn't need a persistent bidirectional connection — just a long-lived HTTP response that the server can keep writing to. If your use case is server-to-client only (notifications, live feeds, progress updates), SSE is usually the simpler choice.

This post covers how to configure Nginx as a reverse proxy in front of an SSE backend, along with a client-side wrapper for handling reconnection.

Why Nginx Needs Special Config for SSE

By default, Nginx buffers responses from upstream servers. This breaks SSE because the client won't receive events until Nginx decides to flush its buffer. You need to explicitly disable buffering and adjust timeouts for the long-lived connections SSE requires.

Nginx Configuration

Global Settings

Start with basic worker and connection tuning:

nginx
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    multi_accept on;
    use epoll;
}

The epoll event driver is important on Linux for handling many concurrent connections efficiently — exactly what SSE needs.

HTTP Block

nginx
http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;

    gzip on;
    gzip_types text/plain text/css application/json application/javascript;
}

Note that gzip compression should not be applied to SSE responses. We'll handle that per-location.

Upstream Definition

nginx
upstream backend_servers {
    server 127.0.0.1:8080;
    server 127.0.0.1:8081 backup;
    keepalive 32;
}

SSE Location Block

This is the critical part:

nginx
location /api/notifications/stream {
    proxy_pass http://backend_servers;

    # Essential SSE settings
    proxy_http_version 1.1;
    proxy_set_header Connection '';
    proxy_buffering off;

    # Timeouts — SSE connections are long-lived
    proxy_connect_timeout 60s;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;

    # Headers
    add_header Content-Type text/event-stream;
    add_header Cache-Control no-cache;
    add_header X-Accel-Buffering no;

    # CORS
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
}

Key points:

  • proxy_buffering off — prevents Nginx from buffering the response body. Without this, events won't reach the client in real time.
  • X-Accel-Buffering: no — tells Nginx's own buffering layer to stay off. Some setups require this in addition to proxy_buffering off.
  • proxy_set_header Connection '' — clears the Connection header so keepalive works correctly with HTTP/1.1.
  • Long read/send timeouts — SSE connections can stay open for minutes or hours. The default 60s timeout will kill them.

Client-Side Reconnection Wrapper

Browsers' native EventSource API handles basic reconnection, but you get more control with a wrapper:

javascript
class EventSourceWrapper {
    constructor(url, options = {}) {
        this.url = url;
        this.options = options;
        this.eventSource = null;
        this.retryCount = 0;
        this.maxRetries = options.maxRetries || 3;
        this.retryDelay = options.retryDelay || 3000;

        this.connect();
    }

    connect() {
        this.eventSource = new EventSource(this.url);

        this.eventSource.onmessage = (event) => {
            this.retryCount = 0;
            if (this.options.onMessage) {
                this.options.onMessage(event);
            }
        };

        this.eventSource.onerror = (error) => {
            this.handleError(error);
        };
    }

    handleError(error) {
        if (this.retryCount < this.maxRetries) {
            setTimeout(() => {
                this.retryCount++;
                this.connect();
            }, this.retryDelay);
        } else if (this.options.onMaxRetriesReached) {
            this.options.onMaxRetriesReached(error);
        }
    }

    close() {
        if (this.eventSource) {
            this.eventSource.close();
        }
    }
}

The retry counter resets on successful message receipt, so transient glitches don't eat into your retry budget.

Monitoring

Add a health check endpoint and dedicated SSE logging:

nginx
location /health {
    access_log off;
    return 200 'OK';
}

log_format sse '$time_local $request_uri $status $request_time';
access_log /var/log/nginx/sse_access.log sse;

The $request_time field is useful for spotting connections that are timing out or hanging unexpectedly.

Quick Checklist

  • proxy_buffering off is set on the SSE location
  • X-Accel-Buffering: no is added as a response header
  • Read and send timeouts are long enough for your use case
  • Gzip is not applied to the SSE content type
  • CORS headers are present if the frontend is on a different origin
  • Health check endpoint exists for load balancer probing
分享: