TL;DR
Ktor WebSockets gives you full-duplex, real-time channels in ~10 lines of Kotlin. install(WebSockets), drop a webSocket("/echo") route inside routing { }, and consume incoming with a for loop. Because the plugin is coroutine-native, you skip thread pools, callback chains, and reactive bridges. For chat and live dashboards, a single SharedFlow handles broadcasting to every connected client. And on Ktor 3, the framework boots in 0.8 seconds and holds 180MB at 10K concurrent connections — numbers Spring Boot can't match without virtual threads and a lot of tuning.
Build your first WebSocket endpoint
JetBrains' Kotlin team just resurfaced the official tutorial for shipping a first Ktor WebSocket endpoint. The promise: real-time apps — chat, dashboards, multiplayer games — with the same ergonomics as a regular Ktor route. The full guide lives at ktor.io/docs, with both server and client docs refreshed on April 24, 2026.
Minimal echo server, end to end:
// build.gradle.kts
dependencies {
implementation("io.ktor:ktor-server-websockets")
}
// Application.kt
import io.ktor.server.application.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import io.ktor.websocket.*
import kotlin.time.Duration.Companion.seconds
fun Application.module() {
install(WebSockets) {
pingPeriod = 15.seconds
timeout = 15.seconds
maxFrameSize = Long.MAX_VALUE
masking = false
}
routing {
webSocket("/echo") {
send("Connection established")
for (frame in incoming) {
if (frame is Frame.Text) {
val text = frame.readText()
if (text.equals("bye", ignoreCase = true)) {
close(CloseReason(CloseReason.Codes.NORMAL, "Bye"))
} else {
send("You said: $text")
}
}
}
}
}
}That's the whole thing. Test with wscat -c ws://localhost:8080/echo.
Why it matters
HTTP request/response breaks down whenever content is generated incrementally, changes in response to events, or has to fan out from one client to many. WebSockets exist precisely for that gap: share trading, ticket grabs, multiplayer state sync, chat. The Ktor plugin maps the WebSocket spec onto Kotlin idioms you already know — send() is a suspend function, incoming is a ReceiveChannel you iterate, close() sends the close frame. The four browser events (open, message, close, error) collapse into one block of straight-line coroutine code:
onConnect→ the start of yourwebSocket { }blockonMessage→incoming.receive()orfor (frame in incoming)onClose→incomingcloses, the loop exits naturallyonError→ an exception thrown inside the block
Technical facts
Configuration knobs you can pass to install(WebSockets):
pingPeriod/pingInterval— auto-ping to keep idle connections alivetimeout— write/ping timeout, then closemaxFrameSize— cap to avoid OOM from oversized payloadsmasking— RFC frame masking on/off (server)contentConverter— e.g.KotlinxWebsocketSerializationConverter(Json)so you cansendSerialized()a typed Kotlin data class- Built-in Deflate extension for payload compression
Frames you'll actually see: Frame.Text, Frame.Binary, Frame.Close, Frame.Ping, Frame.Pong. The incoming channel filters out the control frames by default; if you need raw access (custom ping handling, fragment reassembly), use webSocketRaw instead of webSocket.
Comparison: Ktor 3 vs Spring Boot 3
Independent benchmark from MVP Factory measuring a mobile-backend workload at 10K concurrent connections:
| Metric | Ktor 3.0 | Spring Boot 3.2 (Virtual Threads) | Spring Boot 3.2 (WebFlux) |
|---|---|---|---|
| Cold start to first response | 0.8s | 3.2s | 3.5s |
| Memory at idle | 45MB | 120MB | 135MB |
| Memory at 10K concurrent | 180MB | 410MB | 390MB |
| p99 latency (10K conn) | 12ms | 18ms | 15ms |
| Throughput (req/s, sustained) | 48,200 | 41,500 | 44,100 |
The deeper architectural win is structured concurrency. If a downstream call inside a Ktor handler throws, child coroutines cancel automatically — no leaked threads, no orphan HTTP connections. Spring Boot 3 with virtual threads gets you concurrency, but its StructuredTaskScope is still a preview API in Java 21. Kotlin coroutines have been stable since 1.0.
Use cases & the SharedFlow broadcast pattern
The killer pattern for chat, live dashboards, and multiplayer rooms is broadcasting one event to every connected session without manually tracking a connection list:
val messages = MutableSharedFlow<String>(replay = 0, extraBufferCapacity = 64)
routing {
webSocket("/chat") {
// Each client collects from the shared flow
val job = launch {
messages.collect { msg -> send(msg) }
}
runCatching {
for (frame in incoming) {
if (frame is Frame.Text) messages.emit(frame.readText())
}
}
job.cancel()
}
}No ConcurrentHashMap<Session, _>. No iterating a list to fan-out. SharedFlow handles concurrency, replay, backpressure, and disconnect cleanup natively.
Who gets the most value:
- Kotlin-fluent teams shipping API-focused backends — same idioms in handler, business logic, and tests
- Kotlin Multiplatform teams — the Ktor client is KMP, so your iOS, Android, JS, and JVM clients call the same
webSocket()function - Container/serverless deployments — the 0.8s cold start cuts real money on Kubernetes with aggressive HPA, and 180MB RSS at 10K connections means you fit more pods per node
Limitations & pricing
Things to know before you commit:
- Not every client engine supports WebSockets. Pick CIO, OkHttp, Java, or JS-WS depending on platform — check the engines table in the docs.
- OkHttp ignores the plugin's
pingInterval. Configure ping inside the OkHttp engine block instead. - Control frames are hidden. The default
incomingchannel only delivers data frames. UsewebSocketRawwhen you need ping/pong/close visibility or fragment reassembly. - For one-way streams, prefer SSE. Server-Sent Events is simpler if the server never has to receive from the client.
- Spring still wins on ecosystem. If you need Spring Security OAuth2, Spring Data for six databases, or Actuator out of the box, rebuilding those in Ktor costs engineering months.
Pricing: Ktor is free and open source under Apache 2.0, maintained by JetBrains. The artifacts you need are io.ktor:ktor-server-websockets on the server and io.ktor:ktor-client-websockets on the client. Add ktor-server-content-negotiation + ktor-serialization-kotlinx-json if you want typed JSON over the wire.
What's next
Ktor 3 is the active line, and the WebSocket docs were last touched on April 24, 2026 — this is not a sleeping plugin. The JetBrains team continues to push deeper Kotlin Multiplatform server work (native server targets beyond JVM) and tighter integration between WebSockets and SSE for one-way streams. If you're picking a real-time stack today and your team writes Kotlin, this is the lowest-friction path.
Open the official tutorial, generate a starter project on start.ktor.io with the WebSockets plugin checked, and ship your echo endpoint in an afternoon.
Sources: Ktor server tutorial, Ktor server WebSockets, Ktor client WebSockets, MVP Factory benchmark, Digma comparison, Lanky Dan blog.
