APIs REST resolvem muito bem consultas, cadastros e integrações previsíveis. Mas alguns produtos precisam de uma conversa aberta entre cliente e servidor: chat, painel administrativo com eventos ao vivo, sala de votação, dashboard operacional, colaboração em documento, rastreamento de entrega, jogo simples ou notificação que não pode esperar o próximo refresh. Para esses cenários, WebSockets com Ktor e Kotlin são uma alternativa direta, idiomática e alinhada com coroutines.
Este tutorial mostra como criar um endpoint WebSocket no Ktor, receber mensagens, transmitir eventos para usuários conectados e preparar o caminho para produção. Se você ainda está montando a base do backend, comece pelo tutorial de Ktor para APIs REST e pelo guia completo de Ktor para backend. Aqui vamos focar na parte em tempo real.
Quando WebSocket faz sentido
WebSocket mantém uma conexão aberta e bidirecional. O cliente pode enviar mensagens a qualquer momento, e o servidor também pode empurrar eventos sem esperar uma nova requisição HTTP. Isso é diferente de polling, onde o cliente pergunta repetidamente “tem novidade?”, e diferente de Server-Sent Events, que costuma ser mais simples quando o fluxo é apenas servidor para cliente.
Use WebSocket quando você precisa de:
- comunicação bidirecional com baixa latência;
- eventos frequentes e pequenos;
- presença de usuários conectados;
- salas, canais ou tópicos compartilhados;
- confirmação rápida de mensagens;
- experiência interativa em navegador, Android ou desktop.
Evite WebSocket quando uma requisição REST resolve bem o problema. Manter conexões abertas tem custo operacional: timeout, autenticação, reconexão, balanceamento, observabilidade e limites por usuário precisam ser pensados desde cedo.
Dependências do projeto
Em um projeto Ktor com Gradle Kotlin DSL, adicione o plugin de WebSockets e serialização JSON:
plugins {
kotlin("jvm") version "2.0.0"
id("io.ktor.plugin") version "2.3.12"
kotlin("plugin.serialization") version "2.0.0"
}
dependencies {
implementation("io.ktor:ktor-server-core-jvm")
implementation("io.ktor:ktor-server-netty-jvm")
implementation("io.ktor:ktor-server-websockets-jvm")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
}
As versões acima são didáticas. Em projeto real, confira as versões atuais no catálogo do time e evite misturar versões incompatíveis de Kotlin, Ktor e kotlinx.serialization. Se você usa Version Catalog, veja também o artigo sobre Gradle Version Catalog em Kotlin.
Instalando o plugin WebSockets
No Ktor, WebSocket é um plugin instalado na aplicação. A configuração mínima define ping periódico e timeout para detectar conexões mortas:
import io.ktor.server.application.*
import io.ktor.server.plugins.websocket.*
import kotlin.time.Duration.Companion.seconds
fun Application.module() {
install(WebSockets) {
pingPeriod = 20.seconds
timeout = 20.seconds
maxFrameSize = Long.MAX_VALUE
masking = false
}
configurarRotasTempoReal()
}
pingPeriod ajuda o servidor a perceber quando o cliente sumiu sem fechar a conexão corretamente, algo comum em redes móveis. timeout define quanto tempo esperar por resposta. Em produção, não deixe maxFrameSize infinito sem necessidade: limite o tamanho da mensagem para evitar abuso e consumo de memória.
Criando um endpoint de chat
O exemplo abaixo cria uma sala simples. Cada conexão recebe mensagens de texto e retransmite para todas as outras sessões conectadas. É um ponto de partida para entender o fluxo, não uma arquitetura final de produto.
import io.ktor.server.application.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import io.ktor.websocket.*
import java.util.Collections
private val sessoes = Collections.synchronizedSet(mutableSetOf<DefaultWebSocketServerSession>())
fun Application.configurarRotasTempoReal() {
routing {
webSocket("/ws/chat") {
sessoes += this
try {
send("Conectado ao chat Kotlin Brasil")
for (frame in incoming) {
if (frame is Frame.Text) {
val texto = frame.readText().trim()
if (texto.isNotBlank()) {
transmitir("usuario: $texto")
}
}
}
} finally {
sessoes -= this
}
}
}
}
private suspend fun transmitir(mensagem: String) {
sessoes.toList().forEach { sessao ->
sessao.send(mensagem)
}
}
A ideia principal é que incoming funciona como um fluxo de frames recebidos. Dentro do loop, você trata texto, binário, ping, pong ou close conforme o protocolo do seu produto. Para chat e notificações, texto com JSON costuma ser suficiente.
Use mensagens tipadas com kotlinx.serialization
Em vez de transmitir strings soltas, modele contrato. Isso facilita evolução, validação e testes:
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
sealed interface EventoChat {
@Serializable
@SerialName("mensagem")
data class Mensagem(
val usuario: String,
val texto: String,
val enviadaEm: Long,
) : EventoChat
@Serializable
@SerialName("presenca")
data class Presenca(
val usuario: String,
val online: Boolean,
) : EventoChat
}
private val json = Json {
ignoreUnknownKeys = true
classDiscriminator = "tipo"
}
Com esse modelo, o cliente sabe se recebeu mensagem, presença ou outro evento futuro. Também fica mais fácil integrar com Android, Compose Multiplatform ou frontend web sem depender de parsing frágil.
Autenticação e salas
Não confie em usuario vindo do corpo da mensagem. Em produção, autentique antes de aceitar a conexão. Uma abordagem comum é enviar um token curto no header Authorization ou na query string apenas se o ambiente não permitir headers customizados. O servidor valida o token, identifica o usuário e decide quais salas ele pode acessar.
Um desenho mais realista separa responsabilidades:
ConnectionRegistryguarda conexões por usuário e sala;ChatServicevalida comandos e aplica regras de negócio;MessageRepositorypersiste histórico quando necessário;EventPublisherpublica eventos para outras instâncias do serviço.
Essa separação evita colocar regra de negócio diretamente dentro do bloco webSocket. O bloco deve orquestrar conexão, leitura, escrita e fechamento; a decisão de domínio deve ficar em serviços testáveis.
Cuidado com múltiplas instâncias
O exemplo com sessoes em memória funciona em uma única instância. Se você roda o Ktor em Kubernetes, Cloud Run, VPS com balanceador ou qualquer ambiente com réplicas, um usuário pode estar conectado na instância A enquanto outro está na instância B. Nesse caso, retransmitir apenas para a memória local não basta.
Para escalar, use um barramento externo: Redis Pub/Sub, Kafka, RabbitMQ, NATS ou outro sistema de mensageria. O endpoint WebSocket recebe a mensagem, valida, publica no barramento e cada instância repassa para suas conexões locais. O artigo sobre Kotlin com Kafka e RabbitMQ aprofunda esse tipo de arquitetura.
Também revise afinidade de sessão. Alguns balanceadores permitem sticky sessions, mas isso não substitui um barramento quando o produto precisa entregar eventos entre usuários conectados em réplicas diferentes.
Testes e observabilidade
Teste WebSocket em três camadas. Primeiro, teste serviços puros: validação de comando, permissões, formatação de evento e persistência. Depois, teste o endpoint Ktor com cliente WebSocket de teste. Por fim, rode um teste manual ou automatizado em ambiente de staging para cobrir proxy, TLS e timeout real.
Métricas úteis incluem:
- conexões abertas por instância;
- mensagens recebidas e enviadas por sala;
- erros de parsing e autenticação;
- tempo médio de conexão;
- desconexões por timeout;
- tamanho médio de payload.
Logs devem incluir identificador de usuário, sala e correlation id, mas nunca token, senha ou dados sensíveis. Para aprofundar essa disciplina, veja observabilidade em Kotlin e o guia de Ktor Client resiliente, que discute timeout, fallback e rastreabilidade em integrações HTTP.
Checklist antes de produção
Antes de publicar um WebSocket Ktor em produção, revise:
- Autenticação obrigatória e autorização por sala ou recurso.
- Limite de tamanho de mensagem.
- Ping, timeout e política de reconexão documentados para o cliente.
- Validação de JSON e rejeição de tipo desconhecido.
- Backpressure ou limite de fila por conexão lenta.
- Métricas de conexão, erro e throughput.
- Estratégia para múltiplas instâncias.
- Teste com proxy/TLS igual ao ambiente real.
Esse checklist evita a armadilha de tratar WebSocket como “apenas uma rota diferente”. Na prática, ele muda o ciclo de vida da conexão e cria uma superfície operacional nova.
Conclusão
Ktor torna WebSockets em Kotlin bastante acessíveis: instalar o plugin, criar uma rota webSocket, ler frames e enviar respostas é simples. O valor real aparece quando você combina essa simplicidade com contratos tipados, coroutines, autenticação, observabilidade e uma estratégia clara para escalar além de uma única instância.
Se seu projeto precisa de chat, presença, notificações ou eventos em tempo real, comece pequeno com uma sala simples, teste bem o protocolo e evolua para mensageria externa quando houver múltiplas réplicas. Esse caminho mantém o código Kotlin idiomático sem ignorar as exigências de produção.