DEV Community

Cover image for Server-Sent Events as Your Mobile Real-Time Layer
SoftwareDevs mvpfactory.io
SoftwareDevs mvpfactory.io

Posted on • Originally published at mvpfactory.io

Server-Sent Events as Your Mobile Real-Time Layer

---
title: "SSE on Ktor: Replace 90% of Your WebSocket Use Cases"
published: true
description: "Build a production-ready Server-Sent Events backend with Ktor and a shared KMP client  automatic reconnection, Last-Event-ID replay, and zero custom sync logic."
tags: kotlin, android, mobile, api
canonical_url: https://blog.mvpfactory.co/sse-on-ktor-replace-90-percent-of-your-websocket-use-cases
---

## What We Will Build

Let me show you a pattern I use in every project that needs real-time server-to-client updates. We will build an SSE-powered order tracking endpoint in Ktor and a shared Kotlin Multiplatform client that runs identically on Android and iOS. By the end, you will have automatic reconnection, missed-event replay via `Last-Event-ID`, and backpressure handled by Kotlin Flows — all in roughly 30 lines of server code.

## Prerequisites

- Kotlin 1.9+ and Ktor 3.x (server and client)
- Kotlin Multiplatform project targeting Android and iOS
- Basic familiarity with Kotlin Flows and coroutines
- Ktor SSE plugin (`io.ktor:ktor-server-sse` and `io.ktor:ktor-client-sse`)

## Step 1: Understand Why SSE, Not WebSockets

Before writing code, here is the decision framework. Most mobile real-time features are unidirectional: the server pushes, the client renders. SSE is purpose-built for this.

| Capability | SSE | WebSocket |
|---|---|---|
| Direction | Server → Client | Bidirectional |
| Auto-reconnect | Built into spec | Manual implementation |
| Message replay | `Last-Event-ID` header | You build it yourself |
| Works through CDNs | Yes | Often needs config |
| Server complexity | ~30 lines Ktor | ~80 lines + ping/pong + state |

If your client is not streaming data *upstream* continuously, you are paying a complexity tax for nothing.

## Step 2: Build the Ktor SSE Endpoint

Here is the minimal setup to get this working. Install the SSE plugin and create a route backed by a Flow:

Enter fullscreen mode Exit fullscreen mode


kotlin
fun Route.sseOrderUpdates() {
sse("/orders/{orderId}/events") {
val orderId = call.parameters["orderId"] ?: return@sse
val lastEventId = call.request.headers["Last-Event-ID"]?.toLongOrNull()

    orderRepository.updatesFrom(orderId, afterSequence = lastEventId)
        .collect { event ->
            send(ServerSentEvent(
                data = json.encodeToString(event.payload),
                event = event.type,
                id = event.sequence.toString()
            ))
        }
}
Enter fullscreen mode Exit fullscreen mode

}


The critical piece is `Last-Event-ID`. When a client reconnects after a network drop, after Android Doze, after iOS suspends the app, it sends this header automatically. Your server replays from that sequence number. Zero client-side replay logic.

The `updatesFrom` function returns a `Flow<OrderEvent>` starting after the given sequence. Backpressure comes free from Flow's suspension: if the client cannot consume fast enough, the coroutine suspends. No buffer overflow, no dropped messages.

## Step 3: Build the KMP Shared Client

This is where Kotlin Multiplatform earns its keep. One SSE consumer, both platforms:

Enter fullscreen mode Exit fullscreen mode


kotlin
class SseEventSource(private val httpClient: HttpClient) {

fun connect(url: String, lastEventId: String? = null): Flow<SseEvent> = flow {
    httpClient.sse(url) {
        lastEventId?.let { header("Last-Event-ID", it) }
    } {
        incoming.collect { event ->
            emit(SseEvent(
                id = event.id,
                type = event.event,
                data = event.data
            ))
        }
    }
}.retry(3) { cause ->
    cause is IOException
    delay(1000)
    true
}
Enter fullscreen mode Exit fullscreen mode

}


This runs identically on Android and iOS. When Android kills the connection during Doze mode, `retry` reconnects and the server replays from `Last-Event-ID`. On iOS, the same flow resumes on Background App Refresh. No platform-specific reconnection code.

## Step 4: Know the Boundary

SSE does not replace everything. Here is the honest line:

| Use Case | Right Choice |
|---|---|
| Notifications, live feeds, order tracking | **SSE** |
| Sync streams, server-push updates | **SSE** |
| Collaborative editing (multi-cursor) | WebSocket |
| Multiplayer games, binary streaming | WebSocket |
| High-frequency client-to-server input | WebSocket |

If the client streams structured data *upstream* continuously, use WebSockets. For roughly 90% of mobile real-time features, SSE is simpler and more resilient.

## Gotchas

**The docs do not mention this, but** `Last-Event-ID` only works if you actually assign `id` to every event. Skip the `id` field and replay silently breaks. Always use monotonic sequence numbers from your event store.

**Retry timing is server-controlled.** Send a `retry:` field in your SSE stream to tell clients how long to wait before reconnecting. The default is browser-dependent and usually too aggressive for mobile on spotty networks. Set it explicitly.

**Android Doze mode does not just throttle — it kills connections.** SSE handles this gracefully because reconnection is protocol-level. WebSocket implementations require custom heartbeat intervals, exponential backoff, and state reconciliation. That is the bug factory you do not need.

**Flow backpressure means slow clients will not crash your server**, but they *will* hold a coroutine suspended. Set a reasonable timeout on the server side so zombie connections do not accumulate.

## Conclusion

Default to SSE, not WebSockets. Audit your real-time features: if data flows server-to-client, SSE with `Last-Event-ID` gives you reconnection and replay with zero custom infrastructure. Build the client once in KMP with retry semantics, and both platforms handle connection lifecycle identically. Switch to WebSockets only when you hit a genuinely bidirectional requirement.

**Resources:**
- [Ktor SSE Plugin Docs](https://ktor.io/docs/server-server-sent-events.html)
- [MDN: Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
- [Kotlin Flows Guide](https://kotlinlang.org/docs/flow.html)
Enter fullscreen mode Exit fullscreen mode

Top comments (0)