DEV Community

SoftwareDevs mvpfactory.io
SoftwareDevs mvpfactory.io

Posted on • Originally published at mvpfactory.io

WebSocket Multiplexing Over HTTP/2 for Mobile APIs: Replacing Polling with Structured Streams at Scale

---
title: "WebSocket vs HTTP/2 Streams for Mobile APIs: Handling 50k Concurrent Connections"
published: true
description: "Learn how HTTP/2 multiplexed streams replace WebSockets for real-time mobile APIs  covering stream prioritization, flow control, and 50k concurrent connections with Ktor and Hono."
tags: kotlin, mobile, api, architecture
canonical_url: https://mvpfactory.co/blog/http2-streams-mobile-apis-50k-connections
---

## What We Are Building

By the end of this tutorial, you will understand how to replace raw WebSocket connections with HTTP/2 multiplexed streams for real-time mobile APIs. We will walk through stream prioritization, flow control semantics, and working backend implementations in both Ktor (Kotlin/JVM) and Hono (TypeScript/Bun) — the architecture that gets you to 50k concurrent mobile connections on a single node without the per-connection overhead that kills WebSocket-based systems.

## Prerequisites

- Basic familiarity with HTTP/2 concepts (connections, frames, streams)
- Kotlin experience for the Ktor section; TypeScript for the Hono section
- Flutter basics for the client-side example
- A server environment with TLS termination (HTTP/2 requires HTTPS in practice)

---

## Why WebSockets Break Down at Scale

The default playbook: polling feels slow, so you reach for WebSockets. WebSockets feel modern, so you build on them. Then you hit 10k concurrent users and suddenly you are managing thousands of TCP connections, custom heartbeat logic, reconnection state machines on the client, and a load balancer that has no idea what to do with persistent connections.

The numbers are not subtle. A naive WebSocket server maintains one TCP connection per client. At 50k concurrent mobile users, that is 50k open sockets — each carrying its own kernel buffer overhead, TLS session state, and keepalive timers.

HTTP/2 does not eliminate connections, but it changes the economics of what each connection carries.

---

## How HTTP/2 Multiplexing Changes the Equation

HTTP/2 runs multiple logical streams over a single TCP connection. Each stream is an independent, bidirectional sequence of frames. For mobile APIs, this means:

- One TLS handshake per client, not one per subscription
- Stream-level flow control without application-level throttle logic
- Header compression (HPACK) across streams sharing the same connection
- Priority weighting so critical event streams preempt telemetry or analytics frames

Connection fan-out happens at the stream layer, not the socket layer. Your infrastructure sees far fewer file descriptors. Your mobile client handles reconnection with standard HTTP/2 semantics.

---

## Step 1 — Set Your Stream Priority Hierarchy

Set this early. It is a one-time architectural decision that matters when mobile clients hit constrained or congested networks — and they will.

HTTP/2 assigns each stream a weight (1–256) and an optional dependency on a parent stream. Here is the pattern I use in every project:

| Stream Type | Priority Weight | Dependency |
|---|---|---|
| Auth / session events | 256 | Root |
| UI-critical push events | 200 | Root |
| Presence / status updates | 128 | Root |
| Analytics and telemetry | 32 | Root |

Flow control operates at both the connection and stream level via `WINDOW_UPDATE` frames. If a mobile client is backgrounded and its receive window fills, the server backs off that stream without stalling others. This is behavior you would have to build manually with WebSockets.

---

## Step 2 — Backend with Ktor (Kotlin/JVM)

The 50k concurrent connection target reflects a tested configuration on a 16-core, 32 GB instance — your ceiling will vary with payload size and event frequency.

Ktor's CIO engine runs on coroutines, not threads. Each HTTP/2 stream maps to a suspended coroutine — lightweight concurrency without thread-per-connection cost.

Enter fullscreen mode Exit fullscreen mode


kotlin
embeddedServer(CIO, port = 8443) {
install(Http2)
routing {
get("/events/{clientId}") {
call.respondBytesWriter(contentType = ContentType.Text.EventStream) {
eventFlow(call.parameters["clientId"]!!)
.collect { event ->
writeStringUtf8("data: ${event.toJson()}\n\n")
flush()
}
}
}
}
}.start(wait = true)


The `respondBytesWriter` keeps the HTTP/2 stream open. Flow control is handled by the CIO engine's window management — no custom heartbeat loop required.

---

## Step 3 — Backend with Hono (TypeScript/Bun)

Here is the minimal setup to get this working on the TypeScript side. Hono exposes HTTP/2 natively on Bun and Cloudflare Workers. The `streamSSE` helper manages framing and keeps the connection alive without manual flush logic.

Enter fullscreen mode Exit fullscreen mode


typescript
const app = new Hono()

app.get('/events/:clientId', (c) => {
const clientId = c.req.param('clientId')

return streamSSE(c, async (stream) => {
for await (const event of eventFlow(clientId)) {
await stream.writeSSE({
data: JSON.stringify(event),
event: event.type,
})
}
})
})

export default app


Both implementations share the same architectural contract: a long-lived HTTP/2 stream per client, server-driven push, no upgrade negotiation.

---

## Step 4 — The Flutter Client

Here is the gotcha that will save you hours: you do not need a WebSocket plugin or a special SSE library. The `http` package negotiates HTTP/2 via ALPN automatically over HTTPS. Just handle chunked response streaming directly.

Enter fullscreen mode Exit fullscreen mode


dart
final client = http.Client();
final request = http.Request('GET', Uri.parse('https://api.example.com/events/$clientId'));
final response = await client.send(request);

response.stream
.transform(utf8.decoder)
.transform(const LineSplitter())
.where((line) => line.startsWith('data: '))
.map((line) => jsonDecode(line.substring(6)))
.listen((event) => _handleEvent(event));


The connection is multiplexed with other HTTPS requests your app makes to the same origin — you get the multiplexing benefit at zero additional cost.

---

## Gotchas

**Do not build on HTTP/2 Server Push.** Chrome deprecated it in 2022 after data showed it rarely improved performance and frequently wasted bandwidth. Use long-lived SSE streams or bidirectional streaming RPCs (gRPC-Web) instead — both ride the same multiplexed transport without the deprecation risk.

**HTTP/2 SSE is unidirectional by design.** If your use case requires bidirectional, sub-100ms round-trip messaging — collaborative document editing, multiplayer gaming, live chat with typing indicators — WebSockets remain the right tool. The mistake is defaulting to WebSockets for workloads that are overwhelmingly server-to-client.

**Benchmark before you commit.** The docs do not mention this, but the performance gap between WebSockets and HTTP/2 SSE only becomes significant under real connection load. Measure your actual per-connection overhead with your payload sizes and event frequency before making architectural decisions.

**Load balancer configuration matters.** Standard L4 load balancers handle HTTP/2 connections correctly, but ensure your balancer is not aggressively terminating idle streams — mobile clients in the background will have low traffic but active connections.

---

## Conclusion

Before committing to WebSockets, benchmark your actual connection overhead. If your event streams are predominantly server-to-client, HTTP/2 SSE delivers the same latency profile at significantly lower per-connection cost under load.

Ktor (CIO engine) and Hono-on-Bun are both solid first deployment targets for high-concurrency mobile event APIs. Both handle the HTTP/2 framing layer correctly and expose async primitives that keep you out of thread exhaustion territory.

**Further reading:**
- [Ktor CIO engine docs](https://ktor.io/docs/engines.html)
- [Hono streaming docs](https://hono.dev/docs/helpers/streaming)
- [HTTP/2 RFC 7540 — Stream Priority](https://datatracker.ietf.org/doc/html/rfc7540#section-5.3)
- [Chrome's Server Push deprecation post](https://developer.chrome.com/blog/removing-push)
Enter fullscreen mode Exit fullscreen mode

Top comments (0)