🐛 How I Discovered and Fixed a WebSocket Session Bug in Ktor

As part of my video series on Creating a WebSocket Android Application using Ktor in IntelliJ IDEA Ultimate, I was building the backend WebSocket server with Ktor and testing it using Postman.
My WebSocket server had a simple echo logic:
When a message is received, echo it back
When the user types
bye
, respond with a goodbye and close the connection
Here was my route:
webSocket("/ws") {
for (frame in incoming) {
if (frame is Frame.Text) {
val text = frame.readText()
if (text.equals("bye", ignoreCase = true)) {
outgoing.send(Frame.Text("YOU SAID: bye"))
close(CloseReason(CloseReason.Codes.NORMAL, "Client said BYE"))
} else {
outgoing.send(Frame.Text("YOU SAID: $text"))
}
}
}
}
Problem Observed
When I sent the message bye
in Postman, the server responded with:
YOU SAID: bye
But the WebSocket connection didn’t actually close.
This caused confusion during debugging, because I expected the session to close immediately.
Root Cause
The root issue is that Ktor doesn’t automatically exit the coroutine or loop after calling close()
.
Your coroutine is still alive and continues iterating over the incoming
channel.
Key Insight
The for (frame in incoming)
loop will keep running even after you call close()
, unless you explicitly break
or return.
Solution: Add break
After close()
Here’s the fixed version:
webSocket("/ws") {
for (frame in incoming) {
if (frame is Frame.Text) {
val text = frame.readText()
if (text.equals("bye", ignoreCase = true)) {
outgoing.send(Frame.Text("YOU SAID: bye"))
close(CloseReason(CloseReason.Codes.NORMAL, "Client said BYE"))
break // ✅ Exit the loop after closing
} else {
outgoing.send(Frame.Text("YOU SAID: $text"))
}
}
}
}
Summary of the Fix
🟢 Send a message
🟢 Close the connection
✅ Break the loop so your coroutine exits gracefully
What I Learned
Ktor gives full control, which means you’re responsible for coroutine lifecycle
Closing the session doesn’t stop
incoming.consumeEach
orfor (frame in incoming)
automaticallyAlways
break
orreturn
afterclose()
when done
🙌 Recommendation
If you're writing Ktor WebSocket handlers:
✅ Always handle session cleanup explicitly
✅ Call
break
afterclose()
to avoid silent coroutine hanging
🔗 Follow my full series on [Creating a WebSocket Android App Using Ktor] — where I build both the Ktor server and the Jetpack Compose client, step by step!
Subscribe to my newsletter
Read articles from Holy_Dev directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Holy_Dev
Holy_Dev
strong desire for learning new things