android

Server-Sent Events on Android: a lighter stream than WebSocket

Live data on Android usually means polling or WebSocket. There is a middle path that gets ignored: Server-Sent Events, a one-way HTTP stream where the server pushes messages as they happen. No polling overhead, no socket protocol, no second connection to manage. For dashboards, alerts, and any screen that only reads server state, it is the right-sized hammer.

How SSE works under the hood

The client opens a normal HTTP request. The server responds with `Content-Type: text/event-stream`, sets `Cache-Control: no-cache` and `Connection: keep-alive`, and leaves the connection open. From that point, the server writes messages whenever it has something to say.

Each message is a small block of plain text. A `data:` line carries the payload, an `id:` line gives the event an identifier (used for reconnecting after a drop), and an `event:` line names the type. A blank line marks the end of one event and the start of the next. There is no custom protocol, no framing, no upgrade handshake. It is just HTTP, held open.

A Node server typically keeps a `Set` of connected response objects. A POST endpoint lets the app publish a new event, and the server walks the set, writing a formatted block to each client. The critical detail is `res.flushHeaders()` immediately after setting the content type. Many HTTP frameworks buffer the response until there is content, leaving the client staring at a hanging request.

The Android side, without the foot-guns

On Android, the wiring uses Retrofit and OkHttp. Retrofit handles the request, OkHttp handles the transport, and a small streaming reader turns the response into a Kotlin Flow that the UI layer can collect.

Two configuration details matter more than the rest. The first is the `@Streaming` annotation on the Retrofit method. Without it, Retrofit waits for the whole response body before returning, which never happens with an SSE stream. With it, the body is exposed as it arrives. The second is the read timeout. OkHttp's default is 10 seconds—the maximum gap between bytes before the connection is killed. An SSE stream with rare events will exceed that easily, so the read timeout must be set to zero.

The data layer reads the response as a `BufferedSource` and walks it line by line. Each line is matched against the SSE grammar—an `id:`, `event:`, or `data:` field—and the fields are accumulated until a blank line signals a complete event. That event is then emitted into the Flow, where the UI can render it like any other async value.

Our analysis

Most "real-time" features on Android are read-only. A notification feed, a build status, a stock ticker, a support transcript—these all flow one way, and forcing them through WebSocket is paying for capacity you never use. SSE's case rests on three practical advantages over WebSocket.

First, it is plain HTTP. It works through corporate proxies, survives mobile carrier NAT timeouts more gracefully with application-level heartbeat alternatives, and requires no special infrastructure beyond what your REST API already uses. Second, the reconnection story is built in. The optional `id` field lets the client send a `Last-Event-ID` header on reconnect, and the server resumes from the last acknowledged event. WebSocket requires you to build this yourself. Third, the server-side implementation is trivial—a Set of response objects and a POST endpoint. There is no socket registry, no WebSocket server, no handshake to debug.

The trade-offs are real. Anything bidirectional—chat input, collaborative editing, client-initiated requests that arrive faster than polling—belongs on WebSocket or a dedicated messaging layer. SSE also has no binary frame support; if you need to stream large payloads or compressed data, WebSocket's binary frames are a better fit. And while browser support is universal, some older HTTP client libraries do not handle `text/event-stream` cleanly. Retrofit with `@Streaming` does, once you know to add it.

The two gotchas in the Android implementation—missing `@Streaming` and the default read timeout—are one-liners, but only if you know to look for them. That knowledge gap is the main reason SSE feels harder than it is.

Who should care

Use case Recommended transport
Live notifications, alerts SSE
CI/build status dashboard SSE
Stock/price ticker SSE
Server log viewer SSE
Chat, messaging WebSocket
Collaborative editing WebSocket
Bidirectional commands WebSocket
Large binary streams WebSocket

Bottom line

If your Android screen only reads from the server, default to Server-Sent Events before reaching for WebSocket. It is plain HTTP, it works through corporate proxies, and the Retrofit plus OkHttp wiring is a few lines once you remember the `@Streaming` annotation and the read-timeout gotcha. Keep WebSocket for the cases where the client has a real voice.

Frequently asked questions

What is Server-Sent Events?

A one-way HTTP stream where the server pushes text-formatted event messages to the client over a connection that stays open.

How is SSE different from WebSocket?

WebSocket is a full-duplex protocol supporting messages in both directions. SSE is one-way, server to client, and runs over plain HTTP with no upgrade handshake.

Why do I need @Streaming on the Retrofit method?

Without it, Retrofit buffers the entire response before returning, which never finishes on an SSE stream. The annotation exposes the body as it arrives.

What about the OkHttp read timeout?

The default 10-second read timeout kills the connection during quiet stretches between events. Set it to 0 to disable it for streaming endpoints.

Can SSE reconnect after a network drop?

Yes. Including an `id` field in each event lets the client send a `Last-Event-ID` header on reconnect, so the server can resume from where the client left off.

Related