This episode covers Server-Sent Events in Fiber v3: streaming realtime events from server to browser with Content-Type text/event-stream, sendStream for chunks, the EventSource client, keep-alive, and event hooks when the connection opens and closes.

Most HTTP communication is one-shot request-response. Episode 13 covers the exception: Server-Sent Events (SSE) — a way for the server to stream realtime events to the browser over a single HTTP connection that stays open.
SSE is the right choice for notifications, news feeds, or live dashboards. Unlike WebSocket, SSE is one-way (server to client), but far simpler: just plain HTTP, with Content-Type: text/event-stream and the standard EventSource library in the browser.
The SSE protocol is simple: each event is text formatted as field: value. The known fields are data, event, id, and retry:
data: {"message": "hello"}
event: user-joined
data: {"name": "Budi"}
A single data: line carries the payload; a blank line marks the end of an event. The event: field names an event so clients can listen to it specifically. With id:, clients can resend missed events, and retry: sets the reconnection interval.
In Fiber, SSE is implemented via c.SendStream with the right headers:
app.Get("/events", func(c fiber.Ctx) error {
c.Set("Content-Type", "text/event-stream")
c.Set("Cache-Control", "no-cache")
c.Set("Connection", "keep-alive")
c.SendStream(func() ([]byte, error) {
data, _ := json.Marshal(fiber.Map{
"time": time.Now().Format(time.RFC3339),
})
return []byte("data: " + string(data) + "\n\n"), nil
}, fiber.StreamParams{Chunked: true})
return nil
})c.SendStream is called repeatedly to produce chunks; each chunk is formatted as a data: {...} event. The text/event-stream and keep-alive headers tell the browser not to close the connection and not to cache the response.
SSE connections can live a long time. It's important to detect when a client closes the connection and run cleanup code:
app.Get("/feed", func(c fiber.Ctx) error {
connected := true
go monitor(connected) // contoh: monitor status koneksi
c.SendStream(func() ([]byte, error) {
if !c.Context().IsClosed() {
return []byte("data: ping\n\n"), nil
}
connected = false
return nil, io.EOF
}, fiber.StreamParams{Chunked: true})
return nil
})c.Context().IsClosed() returns true when the client has left. By checking it every iteration, the server can stop background work and return io.EOF to end the stream — preventing goroutine leaks.
On the browser side, clients use EventSource — a standard API, no extra libraries:
<script>
const es = new EventSource("/events");
es.addEventListener("message", (e) => {
console.log("event baru:", e.data);
});
es.onerror = () => console.log("koneksi SSE terputus");
</script>new EventSource("/events") opens an SSE connection to the Fiber endpoint. Unnamed events trigger the message listener; named events like user-joined trigger es.addEventListener("user-joined", ...). EventSource handles reconnection automatically according to the retry value.
curl -N http://localhost:3000/eventscurl -N disables buffering so events print to the terminal immediately. You'll see data: {...} lines keep appearing every second — proof the stream is running. Also open /feed in a browser and watch the console: the client receives repeated data: ping.
Key takeaways:
data: line ended by a blank line; can include event, id, and retry.Content-Type: text/event-stream, Connection: keep-alive, Cache-Control: no-cache.c.SendStream with StreamParams{Chunked: true} produces event chunks.c.Context().IsClosed() detects closed connections so goroutines don't leak.EventSource with automatic reconnection.In the next episode, episode 14, we discuss WebSocket — upgrading the connection with the fiber/websocket package, context-based handlers, and integration with browser clients.