| title | Server-Sent Events (SSE) Proxy |
|---|---|
| sidebar_position | 10 |
The gateway provides SSE-aware proxying with per-event flushing, heartbeat injection, and connection management. When a backend responds with Content-Type: text/event-stream, the SSE middleware takes over response writing to ensure proper streaming behavior.
routes:
- id: events-api
path: /events
backends:
- url: http://backend:8080
sse:
enabled: true
heartbeat_interval: 30s
retry_ms: 3000
connect_event: "connected"
disconnect_event: "disconnected"
max_idle: 5m
forward_last_event_id: true| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Enable SSE proxy mode for this route |
heartbeat_interval |
duration | 0 (disabled) |
Send : heartbeat\n\n comment when idle for this duration |
retry_ms |
int | 0 (don't inject) |
Inject retry: <ms>\n\n field on SSE connection start |
connect_event |
string | "" (none) |
Event data to send when SSE connection is established |
disconnect_event |
string | "" (none) |
Event data to send when SSE connection closes |
max_idle |
duration | 0 (no limit) |
Close connection after this idle duration |
forward_last_event_id |
bool | true |
Forward the client's Last-Event-ID header to the backend |
The SSE middleware sits at step 10.5 in the middleware chain (after WebSocket at step 10, before cache at step 11). It wraps the ResponseWriter and inspects the backend's response:
- Non-SSE responses pass through with zero overhead. The wrapper is transparent.
- SSE responses (
Content-Type: text/event-stream) activate SSE mode:- Sets
Cache-Control: no-storeand removesContent-Lengthto prevent downstream buffering - Buffers writes until a complete SSE event boundary (
\n\n) is found - Flushes each complete event individually for real-time streaming
- Tracks active connections and event counts
- Sets
Standard HTTP proxying buffers response data, which breaks SSE streaming. The SSE middleware scans each Write() call for \n\n boundaries (the SSE event delimiter) and flushes after each complete event. Partial events are buffered until the boundary arrives.
When heartbeat_interval is set, the middleware sends SSE comment lines (: heartbeat\n\n) during idle periods. This keeps the connection alive through proxies and load balancers that may close idle connections.
The SSE specification supports automatic reconnection via the retry: field. When retry_ms is configured, the middleware injects retry: <ms>\n\n at the start of each SSE connection. Browsers will use this value as the reconnection delay.
When forward_last_event_id is enabled (default), the client's Last-Event-ID header is forwarded to the backend, allowing it to resume the event stream from where the client left off.
When connect_event is set, the middleware sends a data: <value>\n\n event immediately after the SSE connection is established (after any retry: field). This can be used to confirm the connection or send initial state.
When disconnect_event is set, the middleware sends a data: <value>\n\n event just before the SSE connection closes. This fires when the handler completes or the client disconnects.
The SSE middleware prevents buffering by downstream middleware:
- Cache: SSE responses have
Cache-Control: no-storeinjected, so cache middleware skips them - Compression: Works correctly — compression middleware sees the streaming writes
- Response rules: May evaluate on incomplete data; consider disabling for SSE routes
Step 10.5 — after WebSocket (10), before cache (11). This ensures:
- WebSocket upgrades are handled first (step 10)
- SSE wrapper is in place before cache (11), coalescing (11.5), and other buffering middleware
- Auth, rate limiting, and IP filter all run before SSE (correct)
GET /sse
Returns per-route SSE proxy statistics:
{
"events-api": {
"active_connections": 5,
"total_connections": 142,
"total_events": 8923,
"heartbeats_sent": 456
}
}Fan-out mode maintains a single upstream SSE connection and broadcasts events to all connected clients. This is useful when many clients need the same event stream (e.g., live scores, stock tickers, notifications).
routes:
- id: live-feed
path: /live
backends:
- url: http://event-source:8080/stream
sse:
enabled: true
heartbeat_interval: 30s
fanout:
enabled: true
buffer_size: 256
client_buffer_size: 64
reconnect_delay: 1s
max_reconnects: 0
event_filtering: true
filter_param: event_type| Field | Type | Default | Description |
|---|---|---|---|
fanout.enabled |
bool | false |
Enable fan-out mode |
fanout.buffer_size |
int | 256 |
Ring buffer size for catch-up events |
fanout.client_buffer_size |
int | 64 |
Per-client channel buffer size |
fanout.reconnect_delay |
duration | 1s |
Delay before reconnecting to upstream on disconnect |
fanout.max_reconnects |
int | 0 (unlimited) |
Maximum upstream reconnection attempts |
fanout.event_filtering |
bool | false |
Allow clients to filter events by type |
fanout.filter_param |
string | event_type |
Query parameter name for event type filtering |
-
Hub: A background goroutine connects to the upstream backend and reads SSE events. Events are parsed, stored in a ring buffer, and broadcast to all connected clients.
-
Catch-up: When a new client connects, it receives buffered events from the ring buffer. If the client sends a
Last-Event-IDheader, only events after that ID are sent. -
Reconnection: If the upstream connection drops, the hub automatically reconnects after
reconnect_delay. It sends the last known event ID to the backend for stream resumption. -
Backpressure: Each client has a buffered channel. If a client can't keep up, events are dropped for that client (non-blocking send). The
dropped_eventscounter tracks this. -
Event filtering: When
event_filteringis enabled, clients can pass?event_type=chat,systemto receive only events matching those types.
{
"live-feed": {
"active_connections": 150,
"total_connections": 500,
"total_events": 0,
"heartbeats_sent": 0,
"fanout": {
"hub_connected": true,
"clients": 150,
"buffer_used": 256,
"reconnects": 2,
"dropped_events": 15,
"last_event_id": "evt-12000"
}
}
}heartbeat_intervalmust be >= 0retry_msmust be >= 0max_idlemust be >= 0- SSE and
passthroughare mutually exclusive - SSE and
response_body_generatorare mutually exclusive - Fan-out requires
sse.enabled: true fanout.buffer_sizemust be >= 0fanout.client_buffer_sizemust be >= 0fanout.reconnect_delaymust be >= 0fanout.max_reconnectsmust be >= 0