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 your webSocket { } block
  • onMessageincoming.receive() or for (frame in incoming)
  • onCloseincoming closes, the loop exits naturally
  • onError → an exception thrown inside the block

Technical facts

Configuration knobs you can pass to install(WebSockets):

  • pingPeriod / pingInterval — auto-ping to keep idle connections alive
  • timeout — write/ping timeout, then close
  • maxFrameSize — cap to avoid OOM from oversized payloads
  • masking — RFC frame masking on/off (server)
  • contentConverter — e.g. KotlinxWebsocketSerializationConverter(Json) so you can sendSerialized() 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:

MetricKtor 3.0Spring Boot 3.2 (Virtual Threads)Spring Boot 3.2 (WebFlux)
Cold start to first response0.8s3.2s3.5s
Memory at idle45MB120MB135MB
Memory at 10K concurrent180MB410MB390MB
p99 latency (10K conn)12ms18ms15ms
Throughput (req/s, sustained)48,20041,50044,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 incoming channel only delivers data frames. Use webSocketRaw when 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.