Aplicações Kotlin modernas quase sempre dependem de chamadas HTTP: gateway de pagamento, antifraude, catálogo, autenticação, serviço interno, API de parceiro, feature flag, geocoding, push notification ou modelo de IA. O problema é que rede falha de formas pouco educadas. Uma chamada pode demorar demais, cair no meio da resposta, retornar 429, saturar em horário de pico ou funcionar para 99% dos usuários e travar exatamente no fluxo mais importante do produto.
Por isso, um Ktor Client resiliente não é apenas um detalhe de infraestrutura. Ele é parte da arquitetura da aplicação. Em 2026, times Kotlin que trabalham com backend, Android offline-first ou Kotlin Multiplatform precisam tratar timeout, retry, fallback, circuit breaker e observabilidade como requisitos normais de integração, não como remendos depois do primeiro incidente.
Este guia mostra um caminho pragmático para configurar chamadas HTTP com Ktor Client usando coroutines, limites claros e decisões que reduzem risco em produção. Se você ainda está montando a base server-side, leia também o guia de Kotlin para backend, o tutorial de Ktor para APIs e o artigo sobre observabilidade em Kotlin.
O erro comum: cliente HTTP sem política
O anti-padrão mais frequente é criar um HttpClient genérico, chamar get() ou post() diretamente no repository e deixar cada tela ou endpoint lidar com falhas do jeito que conseguir. Isso funciona em demo, mas cria comportamento imprevisível quando a dependência externa começa a oscilar.
Sem política explícita, perguntas importantes ficam sem resposta:
- qual é o tempo máximo aceitável para a chamada;
- quais erros merecem retry;
- quantas tentativas são razoáveis;
- o que acontece se o serviço parceiro fica fora por cinco minutos;
- qual log ou métrica permite investigar a falha;
- se a operação pode ser repetida sem duplicar efeito colateral.
Em sistemas distribuídos, ausência de decisão também é decisão. Só que normalmente é a pior: esperar demais, repetir demais, esconder erro demais ou derrubar o usuário por uma falha que poderia ter fallback.
Comece pelo timeout correto
Timeout é a primeira camada de resiliência. Sem ele, uma chamada lenta consome coroutine, thread do engine, conexão e atenção operacional por tempo indefinido. No Ktor Client, o plugin HttpTimeout permite separar timeout total da requisição, timeout de conexão e timeout de socket.
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.HttpTimeout
val client = HttpClient(CIO) {
install(HttpTimeout) {
requestTimeoutMillis = 3_000
connectTimeoutMillis = 1_000
socketTimeoutMillis = 2_000
}
}
Esses números não são universais. Um checkout pode exigir resposta rápida; um relatório assíncrono pode tolerar mais tempo; uma chamada para API interna no mesmo cluster deveria ser mais curta que uma chamada para parceiro externo. O importante é escolher intencionalmente e revisar com base em métricas reais.
Também vale evitar um único cliente global para todos os destinos quando as políticas são muito diferentes. Um client para pagamentos pode ter timeout, headers e logs distintos de um client para catálogo público.
Retry não é solução mágica
Retry ajuda quando a falha é temporária. Ele piora o incidente quando a dependência está saturada e todos os consumidores começam a repetir chamadas ao mesmo tempo. A regra prática é: faça retry apenas para erros provavelmente transitórios, com limite baixo e backoff.
No Ktor, o plugin HttpRequestRetry cobre os casos comuns:
import io.ktor.client.plugins.HttpRequestRetry
val client = HttpClient(CIO) {
install(HttpTimeout) {
requestTimeoutMillis = 3_000
connectTimeoutMillis = 1_000
socketTimeoutMillis = 2_000
}
install(HttpRequestRetry) {
maxRetries = 2
retryIf { _, response ->
response.status.value in listOf(429, 502, 503, 504)
}
retryOnExceptionIf { _, cause ->
cause is java.io.IOException
}
exponentialDelay()
}
}
Repare no limite: duas novas tentativas já podem transformar uma chamada em três requests. Em alto volume, isso pesa. Para endpoints críticos, monitore taxa de retry como métrica própria. Se ela sobe, você tem sinal de degradação antes de o erro final aparecer para o usuário.
Outro ponto essencial é idempotência. GET normalmente pode ser repetido. POST de pagamento, criação de pedido ou envio de mensagem não deve ser repetido sem uma chave idempotente. Se a operação produz efeito colateral, envie um identificador único, registre o estado local e combine com o backend o comportamento para duplicidade.
Modele resposta de erro como contrato
Jogar exceção genérica para cima parece simples, mas empurra complexidade para quem chama. Uma abordagem mais saudável é traduzir a chamada externa para um resultado de domínio.
sealed interface ResultadoParceiro<out T> {
data class Sucesso<T>(val valor: T) : ResultadoParceiro<T>
data class Indisponivel(val motivo: String) : ResultadoParceiro<Nothing>
data class Rejeitado(val codigo: String) : ResultadoParceiro<Nothing>
}
class ParceiroClient(private val http: HttpClient) {
suspend fun buscarLimite(clienteId: String): ResultadoParceiro<LimiteDto> {
return try {
val resposta = http.get("https://api.parceiro.example/limites/$clienteId")
when (resposta.status.value) {
200 -> ResultadoParceiro.Sucesso(resposta.body())
404 -> ResultadoParceiro.Rejeitado("cliente_nao_encontrado")
in 500..599 -> ResultadoParceiro.Indisponivel("parceiro_instavel")
else -> ResultadoParceiro.Indisponivel("resposta_inesperada")
}
} catch (e: Exception) {
ResultadoParceiro.Indisponivel("falha_http")
}
}
}
O exemplo é pequeno, mas mostra a ideia: o restante da aplicação não precisa saber se a falha veio de timeout, DNS, 503 ou socket fechado. Ele precisa saber se pode seguir com fallback, pedir nova tentativa ao usuário ou interromper o fluxo.
Quando usar circuit breaker
Circuit breaker é útil quando uma dependência externa está falhando repetidamente e continuar chamando só aumenta latência e pressão. Ele “abre o circuito” por um período, falhando rápido ou usando fallback até que uma janela de teste indique recuperação.
Em projetos JVM, uma escolha comum é usar Resilience4j ao redor da chamada suspensa. Em Kotlin, cuide para não bloquear threads e para manter o contrato de coroutine claro.
class CatalogoService(
private val parceiro: ParceiroClient,
private val cache: CatalogoCache,
) {
suspend fun buscarProduto(id: String): ProdutoResumo {
val resultado = parceiro.buscarProduto(id)
return when (resultado) {
is ResultadoParceiro.Sucesso -> {
cache.salvar(id, resultado.valor)
resultado.valor
}
is ResultadoParceiro.Indisponivel -> {
cache.buscar(id) ?: ProdutoResumo.indisponivel(id)
}
is ResultadoParceiro.Rejeitado -> {
ProdutoResumo.naoEncontrado(id)
}
}
}
}
Nem todo sistema precisa de circuit breaker no primeiro dia. Para muitos produtos, timeout curto, retry controlado, fallback e boas métricas resolvem 80% do problema. Circuit breaker entra quando a dependência é crítica, o volume é alto ou incidentes anteriores mostraram cascata de falhas.
Fallback precisa ser honesto
Fallback não é fingir que tudo está normal. É oferecer uma resposta degradada, segura e compreensível. Em um app de conteúdo, pode ser mostrar cache antigo com aviso de atualização. Em um checkout, pode ser bloquear uma opção de pagamento temporariamente. Em um dashboard interno, pode ser exibir dados parciais.
O pior fallback é silencioso e enganoso. Se a informação está desatualizada, deixe isso claro no modelo. Se uma recomendação não pôde ser calculada, não invente. A confiabilidade percebida do produto depende tanto do comportamento em falha quanto do comportamento em sucesso.
Observabilidade: log, métrica e trace
Resiliência sem observabilidade vira chute. Cada chamada relevante deveria permitir responder: qual destino foi chamado, quanto demorou, quantas tentativas ocorreram, qual status voltou, se o fallback foi usado e qual correlação liga a chamada ao request original.
Para APIs Kotlin, combine logs estruturados, métricas com Micrometer ou OpenTelemetry e traces distribuídos. Se o projeto já usa mensageria, vale conectar essa disciplina ao que foi discutido em Kotlin com Kafka e RabbitMQ: duplicidade, retry e dead letter queue são parentes próximos dos mesmos problemas em HTTP.
Um conjunto mínimo de métricas para client HTTP inclui:
- duração por destino e rota lógica;
- contagem de status
2xx,4xx,5xxe exceções; - quantidade de retries;
- quantidade de fallbacks;
- circuit breaker aberto, meio aberto ou fechado;
- timeout por dependência.
No ecossistema de infraestrutura, muitas ferramentas usadas para observar essas chamadas nasceram em Go. Para comparar modelos de backend e operação, o portal Go Brasil é um bom complemento para quem trabalha com microsserviços, Prometheus, Kubernetes e ferramentas cloud-native.
Checklist para produção
Antes de considerar uma integração HTTP pronta para produção, revise:
- Timeout definido por destino, não herdado por acaso.
- Retry limitado a erros transitórios, com backoff e métrica.
- Operações com efeito colateral protegidas por idempotência.
- Resultado traduzido para contrato de domínio, não exceção genérica espalhada.
- Fallback explícito e honesto quando fizer sentido.
- Logs estruturados sem vazar token, CPF, cartão ou segredo.
- Métricas e tracing suficientes para investigar incidente.
- Testes cobrindo sucesso,
404,429,5xx, timeout e exceção de rede.
Esse checklist também ajuda em entrevistas de backend Kotlin. Ele mostra maturidade além do “sei chamar uma API”: você demonstra que entende falhas reais, custo operacional e experiência do usuário.
Conclusão
Ktor Client é uma ótima ferramenta para integrações HTTP em Kotlin porque combina API idiomática, coroutines e flexibilidade multiplataforma. Mas a qualidade da integração depende das políticas ao redor dele. Timeout evita espera infinita. Retry corrige falhas transitórias sem virar tempestade quando bem limitado. Circuit breaker protege o sistema de cascatas. Fallback preserva experiência quando a dependência degrada. Observabilidade transforma falha em diagnóstico.
Se você está construindo backend Kotlin em 2026, não deixe resiliência para depois. Comece simples, com limites claros e métricas úteis. Depois evolua conforme volume, criticidade e histórico de incidentes. Esse é o caminho para criar aplicações Kotlin que não apenas funcionam no ambiente local, mas continuam se comportando bem quando a rede, os parceiros e a produção fazem o que sempre fazem: falham de vez em quando.