Mobile Core Concepts
Network Protocols
Why the transport you pick shapes latency, reconnection, and battery - and the protocol an interviewer expects you to justify for any live or streaming surface.
Picking the wire protocol is one of the early critical decisions in any mobile design. Caching, retry, reconnect, lifecycle binding, server architecture - all of it follows from "which protocol do we speak, and which version of it." This page covers the six options worth knowing (HTTP/1.1, HTTP/2, HTTP/3, WebSocket, SSE, gRPC) plus long-poll as the legacy fallback, how each is initiated and framed, the head-of-line behavior that distinguishes them, and the Android client libraries that talk each one. Certificate pinning and image-pipeline specifics live in their own Core Concept entries; this page treats them as cross-links.
The transport landscape
Five axes determine the right protocol: direction (client-to-server, server-to-client, both), persistence (one-shot, long-lived), framing (text, binary, structured), multiplexing (one stream per connection, many streams per connection), and reconnect semantics (manual, automatic with offset).
| Protocol | Direction | Persistence | Framing | Multiplexing | Reconnect | Use for |
|---|---|---|---|---|---|---|
| HTTP/1.1 | Request/response | Connection-pooled, short-lived | Text headers + body | One stream per TCP | Manual retry | Plain REST, CDN-backed static |
| HTTP/2 | Request/response | Persistent, one TCP/many streams | Binary frames | Yes - many streams per TCP | Manual retry | High-fan-out REST, mobile-to-CDN |
| HTTP/3 / QUIC | Request/response | Persistent over UDP | Binary frames | Yes - per-stream HoL eliminated | 0-RTT resume | Lossy networks, head-of-line-sensitive |
| WebSocket | Full-duplex | Persistent (single TCP) | Text or binary frames | App-level only | Manual + backoff | Chat, presence, live reactions |
| SSE | Server → client | Persistent (single GET) | Text events (id/event/data) | HTTP/2 multiplexed if available | Automatic via Last-Event-ID | Live feeds, notifications, comment streams |
| gRPC | RPC + streaming | Persistent over HTTP/2 | Protobuf binary | Yes - HTTP/2 streams | Manual + backoff | Strict-schema RPC, internal services |
| Long-poll | Server → client | Many short HTTP requests | Text JSON | One per connection | Implicit (new request) | Legacy fallback behind hostile proxies |
HTTP - versions and what changes
The three HTTP versions are the same protocol semantically (verbs, headers, status codes) but very different on the wire. The mobile-relevant differences live in framing and head-of-line behavior.
HTTP/1.1
Text-framed. One in-flight request per TCP connection at a time. Pipelining was meant to let a client send multiple requests on one connection without waiting, but it requires strict response ordering and most servers/proxies disable it - in practice, 1.1 = one request per connection per round-trip. Clients work around it by opening 6 connections per host (the de-facto browser limit) and round-robining.
Head-of-line at the connection level: a slow response blocks subsequent requests on the same connection until it returns.
HTTP/2
Binary-framed. Many concurrent streams over one TCP connection - up to ~100 typically. Streams are independent at the application layer; a slow stream doesn't block others.
But TCP itself is still ordered. A dropped packet on the underlying TCP connection blocks all streams using that connection until retransmission. This is the "HoL at the TCP layer" tradeoff: better than 1.1 (no app-layer ordering constraint) but not free under packet loss.
Server push exists but is mostly deprecated in practice (low cache hit rate, complex invalidation); don't design around it.
HTTP/3 / QUIC
Runs on UDP. Streams are independent all the way down - a packet drop affects only its own stream, not the others on the same connection. 0-RTT connection resume: the second request after a connection drop can include payload in the handshake itself.
The catch on Android: full HTTP/3 support requires Cronet (Google's HTTP stack) - OkHttp is HTTP/2 only as of writing. For consumer apps on flaky cellular networks, HTTP/3 is meaningfully better; for backend-only apps, the OkHttp investment usually wins on developer ergonomics.
WebSocket
A WebSocket is a single TCP connection that starts as an HTTP/1.1 request and upgrades to a binary frame protocol via the Upgrade: websocket header. Once upgraded, both ends can send messages at any time on the same socket - full-duplex.
val client = OkHttpClient()
val request = Request.Builder().url("wss://api.example.com/ws").build()
val listener = object : WebSocketListener() {
override fun onOpen(ws: WebSocket, response: Response) {
ws.send("""{"type":"subscribe","channel":"post:abc:comments"}""")
}
override fun onMessage(ws: WebSocket, text: String) { dispatch(text) }
override fun onFailure(ws: WebSocket, t: Throwable, r: Response?) {
if (!intentionallyClosed.get()) scheduleReconnect()
}
}
val webSocket = client.newWebSocket(request, listener)Real WebSocket designs almost always layer an application-level multiplexing protocol on top: { "type": "subscribe", "channel": "post:abc:comments" }. One physical WS connection carries many logical channels, dispatched client-side by the channel field. Channel names are usually hierarchical with colons (post:{id}:comments, user:{id}:notifications) and map directly to backend pub/sub topics.
Reconnect with full-jitter backoff
WebSocket has no automatic reconnect - that's the app's responsibility. The standard pattern: exponential backoff with full jitter, gated by an AtomicBoolean intentionallyClosed flag so an explicit webSocket.cancel() doesn't trigger reconnection.
private var reconnectDelay = 1_000L
private val intentionallyClosed = AtomicBoolean(false)
override fun onFailure(ws: WebSocket, t: Throwable, r: Response?) {
if (intentionallyClosed.get()) return
scope.launch {
delay(reconnectDelay)
// Full jitter: random in [0, cap] - prevents thundering herd after server restart
reconnectDelay = Random.nextLong(0, (reconnectDelay * 2).coerceAtMost(32_000))
connect()
}
}Full jitter (not capped exponential) is the right shape because clients tend to disconnect in batches when a server restarts; without jitter, every client reconnects at the same 2 * delay instant and the server falls over again.
A WebSocket opened in a Composable's body leaks after navigation - wrap it in DisposableEffect with onDispose { disconnect() }, not LaunchedEffect, which has no cleanup hook to tear the connection down when the composable leaves the tree.
SSE - Server-Sent Events
One HTTP GET that the server never closes. Frames are newline-delimited text events with optional id, event, and data fields. The browser/client EventSource API auto-reconnects on disconnection and sends the last received id in the Last-Event-ID header so the server can replay missed events.
GET /api/feed/live HTTP/1.1
Accept: text/event-stream
Last-Event-ID: evt_882
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
id: evt_883
event: new_comment
data: {"id":"c99","body":"nice post","author":"alice"}
: keep-aliveSSE is plain HTTP - proxies, load balancers, and CDNs handle it like any other request. WebSocket's Upgrade header is often stripped by hostile proxies. If the design needs server push only (no client → server traffic on the same stream), SSE is the simpler, more proxy-friendly choice.
The : keep-alive comment frame in the example above is essential: proxies and load balancers idle out long-held connections (often at 30-60 s), so the server has to send periodic comment frames or the client sees mystery disconnections it didn't cause.
Android has no native EventSource. The pragmatic implementations: OkHttp with @Streaming (manual frame parsing), or LaunchDarkly's okhttp-eventsource library (handles reconnect + Last-Event-ID for you).
Long-poll
Client sends a request. Server holds the response open until something happens (new data, timeout). When the response returns, the client immediately fires the next request. Server push, simulated with vanilla HTTP/1.1.
You will meet it in two places: legacy backends that pre-date WebSocket adoption, and corporate proxies that strip the Upgrade header. The reasonable fallback ladder for a modern mobile design is: WebSocket → SSE → long-poll, switching down only when the upper option fails to connect (onFailure after handshake).
Don't design a system around long-poll today. Do build the WebSocket / SSE client to gracefully fall back to it.
gRPC streaming
gRPC = Protobuf over HTTP/2. The Android use case is internal services or back-office mobile apps where you control both ends; consumer-facing public APIs almost always stay on REST + JSON because of CDN compatibility and developer familiarity.
Four call shapes:
- Unary - one request, one response. Like REST.
- Server streaming - one request, stream of responses. Like SSE, but binary and on HTTP/2.
- Client streaming - stream of requests, one response. Rare on mobile.
- Bidirectional streaming - full-duplex stream. Like WebSocket, with strict schema.
The win over REST+JSON is payload size and parse cost (Protobuf binary is typically 30-50% smaller and 5-10× faster to parse than JSON), plus compile-time type safety. The cost is debuggability (binary on the wire, no curl inspection), CDN incompatibility (Protobuf payloads can't be edge-cached as text), and SDK weight (the gRPC Android client is ~1 MB).
Android client libraries
| Library | Layer | HTTP version | Best for |
|---|---|---|---|
| OkHttp | HTTP client + connection pool | HTTP/1.1, HTTP/2 | The substrate. Use directly when you need raw interceptor / cache control. |
| Retrofit | Type-safe RPC over OkHttp | Whatever OkHttp speaks | The default for declarative REST. Annotation-driven; delegates to OkHttp. |
| Ktor Client | Kotlin-native, multiplatform | HTTP/1.1, HTTP/2 | KMP projects; cleaner DSL than Retrofit; smaller in shared modules. |
| Cronet | Google's native HTTP stack | HTTP/1.1, HTTP/2, HTTP/3 | Apps that need HTTP/3 on Android. Heavier binary; tighter Google ecosystem fit. |
OkHttp's Interceptor chain is the central extension point - each interceptor sees the request on the way out and the response on the way back. Application-level interceptors run on every request (auth headers, logging); network interceptors run on the actual wire bytes (compression, retries). Token refresh is a separate beast - use Authenticator (called only on 401), with a two-client pattern so the refresh call itself doesn't carry the failing token and re-trigger the authenticator (infinite loop).
For coalesced token refresh under concurrent 401s, the Mutex.withLock pattern in the Concurrency entry is the canonical implementation.
See also
- Certificate pinning - covered in the Security and Crypto Core Concept. The OkHttp
CertificatePinnerandnetwork_security_config.xmlpaths both live there; this entry assumes a TLS connection is already trusted. - Image loading pipelines - the network half (HTTP cache headers, signed-URL TTL, range requests for resume) is here in spirit; the decode + memory side lives in the Image Loading Internals Core Concept.
- Caching - HTTP-level cache semantics (Cache-Control, ETag, stale-while-revalidate) live in the Caching Strategies Core Concept.
Used in
- Messenger App - WebSocket transport for chat, presence, and typing indicators; the channel-multiplexing protocol on top of one WS connection is the spine of the design.
- Newsfeed App - SSE for the live-comment stream layered on top of HTTP/2 for the paginated feed reads; the
Last-Event-IDreplay is what makes "resume on backgrounded app" work without bespoke sync logic. - Photo Gallery App - HTTP
PATCHwithUpload-Offsetfor resumable chunked upload; the protocol choice (HTTP, not WebSocket) is itself a deep-dive in that page. - Networking Client SDK - the dedicated system-design problem on this topic. Designs the OkHttp-shaped library from scratch (interceptors, connection pool, dispatcher).
- Ride-Sharing App - WebSocket for driver location push; the reconnect-with-jitter pattern matters because driver fleets reconnect in coordinated waves.
Done reading? Mark it so it sticks in your dashboard.