Para depurar uma aplicação Tokio com tasks lentas, locks disputados ou futuros que parecem “travados”, use tokio-console junto de console-subscriber. A combinação permite observar tasks, recursos assíncronos, wakes, tempo ocupado e períodos de inatividade em uma TUI. O fluxo mínimo é: instalar tokio-console, inicializar console_subscriber::init(), compilar com --cfg tokio_unstable, executar o serviço e abrir o console em outro terminal.
O tokio-console é especialmente útil quando logs tradicionais dizem apenas que uma requisição começou, mas não explicam por que ela nunca terminou. Ele não substitui tracing e logs estruturados, métricas ou profiling de CPU. Seu papel é mais específico: mostrar como o runtime Tokio está agendando e despertando o trabalho assíncrono.
Este guia apresenta a configuração mínima, um exemplo de contenção com Mutex, como interpretar os principais sinais, quando usar instrument, quais armadilhas geram falsos diagnósticos e como adotar o console sem abrir uma interface de debug perigosa em produção.
Resposta rápida: instalação e uso
| Etapa | Comando ou código | Objetivo |
|---|---|---|
| Instalar a TUI | cargo install --locked tokio-console | Criar o comando tokio-console |
| Adicionar subscriber | cargo add console-subscriber | Exportar dados do runtime |
| Inicializar | console_subscriber::init(); | Ativar coleta e servidor do console |
| Compilar instrumentado | RUSTFLAGS="--cfg tokio_unstable" cargo run | Habilitar eventos detalhados do Tokio |
| Abrir a interface | tokio-console | Conectar e inspecionar tasks |
| Tornar o cfg repetível | .cargo/config.toml | Evitar depender de uma variável manual |
Antes de automatizar, confira tokio-console --help e a documentação da versão de console-subscriber instalada. A instrumentação usa tokio_unstable, portanto uma atualização de Tokio merece validação explícita no projeto.
O problema: async pode esconder onde o tempo foi gasto
Código assíncrono divide trabalho em futuros que avançam quando o executor os consulta. Quando um futuro não pode continuar — porque espera rede, timer, canal ou lock — ele devolve Poll::Pending. Mais tarde, um waker avisa ao runtime que vale a pena consultar aquele futuro novamente.
Esse modelo evita bloquear uma thread para cada conexão, mas cria perguntas diferentes das encontradas em código síncrono:
- a task está esperando I/O legitimamente ou perdeu um wake?
- ela foi despertada milhares de vezes sem produzir progresso?
- passou muito tempo ocupada porque executou trabalho bloqueante?
- está parada em um
Mutexdisputado? - o sistema criou tasks demais para uma operação simples?
- uma task foi criada e nunca encerrada?
- o gargalo está no runtime ou em uma dependência externa?
Um log como este não responde:
INFO iniciando processamento do pedido 42
Se não houver mensagem de conclusão, você sabe que algo ficou no caminho. O tokio-console ajuda a restringir a investigação à task, ao recurso e ao padrão de agendamento envolvidos.
Para revisar os fundamentos antes da ferramenta, consulte async/await em profundidade e o guia completo de Tokio.
Como o tokio-console funciona
A arquitetura tem duas partes:
console-subscriberdentro da aplicação recebe eventos de instrumentação e disponibiliza os dados de diagnóstico;tokio-consoleno terminal conecta-se ao processo instrumentado e renderiza a TUI.
O subscriber se integra ao ecossistema tracing. Além de eventos normais da aplicação, ele usa a instrumentação emitida pelo Tokio quando o binário é compilado com:
--cfg tokio_unstable
O nome “unstable” merece atenção: ele não significa necessariamente que você precisa compilar o projeto com Rust nightly. Significa que essas APIs e garantias de diagnóstico não fazem parte da estabilidade normal do Tokio. Fixe versões quando a previsibilidade for importante e teste a atualização antes de promovê-la.
Configuração mínima em um projeto
Crie um binário de exemplo:
cargo new console-demo
cd console-demo
cargo add tokio --features full
cargo add console-subscriber
cargo install --locked tokio-console
No src/main.rs:
use std::time::Duration;
#[tokio::main]
async fn main() {
console_subscriber::init();
let worker = tokio::spawn(async {
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
println!("worker ativo");
}
});
tokio::time::sleep(Duration::from_secs(30)).await;
worker.abort();
}
Execute a aplicação com a instrumentação:
RUSTFLAGS="--cfg tokio_unstable" cargo run
Em outro terminal:
tokio-console
A interface deve listar a task principal, o worker criado por tokio::spawn e recursos associados aos timers. Se o console não conectar, confirme:
- se
console_subscriber::init()executa antes dosspawnrelevantes; - se a aplicação foi realmente recompilada com
tokio_unstable; - se o processo continua rodando;
- se host e porta foram alterados por configuração;
- se firewall, container ou namespace de rede bloqueiam a conexão.
Configurando tokio_unstable sem depender do shell
Para um projeto dedicado a diagnóstico local, você pode colocar a flag em .cargo/config.toml:
[build]
rustflags = ["--cfg", "tokio_unstable"]
Essa opção é simples, mas afeta todos os builds feitos com aquela configuração. Em um serviço de produção, pode ser melhor manter a instrumentação em um alias, script ou perfil operacional explícito, para não confundir o binário normal com o binário de diagnóstico.
Exemplo de script local:
#!/usr/bin/env bash
set -euo pipefail
RUSTFLAGS="${RUSTFLAGS:-} --cfg tokio_unstable" cargo run --bin api
Se o repositório já define RUSTFLAGS para linker, CPU target ou lints, não sobrescreva a variável sem revisar. Flags de compilação também afetam cache: mudar RUSTFLAGS pode provocar recompilação ampla e reduzir o reaproveitamento do sccache.
Exemplo prático: encontrando contenção em Mutex
Considere uma aplicação que protege estado compartilhado e, por engano, mantém o lock durante uma operação lenta:
use std::{sync::Arc, time::Duration};
use tokio::sync::Mutex;
#[tokio::main]
async fn main() {
console_subscriber::init();
let contador = Arc::new(Mutex::new(0_u64));
for id in 0..20 {
let contador = Arc::clone(&contador);
tokio::spawn(async move {
let mut valor = contador.lock().await;
*valor += 1;
// Problema didático: mantém o lock durante a espera.
tokio::time::sleep(Duration::from_millis(500)).await;
println!("task {id}: contador = {valor}");
});
}
tokio::time::sleep(Duration::from_secs(15)).await;
}
O programa não possui um deadlock clássico, mas serializa vinte tasks atrás do mesmo lock. No console, você pode observar muitas tasks aguardando o recurso enquanto apenas uma progride por vez.
A correção é reduzir a seção crítica:
let valor_atual = {
let mut valor = contador.lock().await;
*valor += 1;
*valor
}; // lock liberado aqui
tokio::time::sleep(Duration::from_millis(500)).await;
println!("task {id}: contador = {valor_atual}");
A lição não é “Mutex assíncrono é ruim”. O problema é manter exclusividade durante um .await que não precisa do estado protegido. O console torna esse padrão visível; a revisão do código explica por que ele acontece.
Como ler os sinais sem adivinhar
A interface evolui, mas os conceitos principais permanecem. Observe a relação entre os sinais, não um número isolado.
Tasks vivas por tempo demais
Uma task antiga pode ser perfeitamente legítima: servidor, consumer de fila, monitor ou loop de manutenção. Investigue quando:
- a quantidade cresce continuamente;
- tasks de request sobrevivem muito depois do timeout esperado;
- a task deveria encerrar após uma operação curta;
- o estado mostra espera em um recurso que nunca progride.
Antes de chamar isso de vazamento, identifique a responsabilidade da task. Loops de background são longevos por projeto.
Busy time alto
Tempo ocupado elevado sugere que a task passou muito tempo sendo consultada ou executando trabalho. Pode indicar:
- loop sem espera suficiente;
- CPU pesada dentro de uma task async;
- processamento síncrono grande entre dois
.await; - polling excessivo causado por wakes frequentes.
Para CPU pesada, mova trabalho bloqueante ou computacional para uma estratégia apropriada, como spawn_blocking, Rayon ou serviço separado, conforme o caso. Depois confirme com profiling; o guia de performance em produção mostra quando usar flamegraph, Criterion e métricas.
Idle time alto
Tempo ocioso não é automaticamente ruim. Uma task que espera uma mensagem por minutos está cumprindo sua função. Torna-se suspeito quando o contrato previa conclusão rápida e nenhuma dependência externa justifica a espera.
Muitos wakes
Wakes frequentes com pouco progresso podem apontar para:
- canal ou stream excessivamente “barulhento”;
- loop que se autoacorda;
- seleção de futuros que recria trabalho continuamente;
- recurso disputado por muitas tasks;
- polling provocado por integração defeituosa.
Não otimize apenas para reduzir o contador. Correlacione wakes, busy time, código executado e throughput real.
Recursos e operações assíncronas
Recursos instrumentados ajudam a enxergar timers, locks, semáforos e outras primitivas. Uma espera longa em Semaphore, por exemplo, pode significar que o limite de concorrência está correto e o tráfego aumentou — ou que permissões não estão sendo liberadas após erros.
Dê nomes e contexto às tasks
Tasks anônimas tornam qualquer sessão de diagnóstico mais difícil. O tracing permite instrumentar futuros com spans que carregam campos úteis.
use tracing::{info_span, Instrument};
for pedido_id in 1..=10 {
tokio::spawn(
async move {
processar_pedido(pedido_id).await;
}
.instrument(info_span!("processar_pedido", pedido_id)),
);
}
Com spans consistentes, você consegue correlacionar a task ao pedido, tenant, fila ou componente sem registrar dados sensíveis. Evite colocar token, e-mail completo, conteúdo de mensagem ou segredo em nomes e campos de diagnóstico.
A combinação recomendada é:
- nome da operação, estável e com baixa cardinalidade;
- identificador técnico, quando permitido pela política de privacidade;
- spans internos para banco, HTTP, fila e cache;
- métricas agregadas para observar a tendência;
- tokio-console para uma sessão interativa quando a tendência vira incidente.
Veja também logging e observabilidade em Rust e OpenTelemetry em produção.
Integrando console-subscriber com tracing normal
Uma aplicação real provavelmente já usa tracing-subscriber para logs. Nesse cenário, não inicialize dois subscribers globais independentes. Monte as camadas em um único registry, conforme as APIs suportadas pelas versões instaladas.
Exemplo conceitual:
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
fn init_observability() {
let console_layer = console_subscriber::spawn();
let fmt_layer = tracing_subscriber::fmt::layer();
tracing_subscriber::registry()
.with(console_layer)
.with(fmt_layer)
.init();
}
A forma exata de configurar filtros, retenção, endereço e capacidade depende da versão. O ponto arquitetural é: um subscriber global composto por camadas, e não múltiplas chamadas concorrentes a .init().
Também separe os filtros. Um filtro agressivo para logs não deve remover eventos necessários ao console. Da mesma forma, habilitar tracing extremamente detalhado de todas as dependências pode gerar ruído e overhead.
tokio-console versus outras ferramentas
| Ferramenta | Pergunta principal | Melhor uso |
|---|---|---|
tokio-console | O que as tasks e recursos do runtime estão fazendo agora? | Locks, wakes, tasks pendentes, excesso de spawn |
tracing | Qual caminho lógico uma operação percorreu? | Requests, filas, banco, contexto de negócio |
| Métricas | O sistema está piorando ao longo do tempo? | Latência, erros, saturação, filas, SLO |
| Flamegraph | Onde a CPU foi consumida? | Hot paths e trabalho computacional |
cargo-llvm-cov | Quais caminhos foram exercitados nos testes? | Lacunas de testes e regressões |
| Miri | A execução interpretada encontra certas violações de memória? | Código unsafe e undefined behavior |
Não use o console para provar que uma API ficou rápida. Faça o diagnóstico, aplique a correção e valide com métricas e benchmark. Para cobertura, consulte cargo-llvm-cov; para comportamento de memória, veja Miri.
Um roteiro de investigação de task travada
Quando o alerta é “requests penduram depois de alguns minutos”, siga um roteiro reproduzível:
- Defina o sintoma: rota, percentil de latência, taxa de erro e janela de tempo.
- Reproduza em ambiente controlado: carga, conjunto de features e configuração semelhantes.
- Ative o console: de preferência por configuração explícita e acesso restrito.
- Filtre as tasks relevantes: procure idade, estado, busy time e recursos aguardados.
- Correlacione com spans: identifique request, worker ou dependência.
- Inspecione locks e limites:
Mutex,RwLock,Semaphore, pool de conexões e canais. - Procure trabalho bloqueante: chamadas síncronas, compressão, parsing grande, filesystem e CPU.
- Aplique uma alteração pequena: reduza seção crítica, limite spawn ou mova CPU.
- Repita a carga: compare console, métricas e latência.
- Crie regressão: teste, benchmark ou alerta que impeça o retorno do problema.
Esse método evita a correção supersticiosa de “aumentar o número de threads do Tokio”. Mais threads podem esconder contenção temporariamente, mas não consertam um lock mantido durante I/O ou uma fila sem backpressure.
Produção: use com controles, não como painel público
O console é uma interface de diagnóstico. Trate-o como uma superfície operacional sensível.
Restringir rede
Prefira bind local, rede privada, sidecar ou túnel autenticado. Não publique a porta diretamente na internet. Dados de tasks e spans podem revelar arquitetura, nomes internos e padrões de tráfego.
Ativação explícita
Uma abordagem segura é habilitar a camada somente quando uma variável de configuração está presente:
fn console_habilitado() -> bool {
std::env::var("TOKIO_CONSOLE_ENABLED").as_deref() == Ok("1")
}
A montagem do subscriber pode então escolher a camada conforme o ambiente. Não use variável para carregar segredo no log; ela apenas controla a feature operacional.
Medir overhead
Instrumentação consome CPU, memória, buffers e rede. O impacto depende do volume de tasks, eventos, retenção e carga. Faça um teste com tráfego representativo antes de manter a camada ativa por longos períodos.
Limitar duração
Para incidentes, defina uma janela: ativar, capturar, diagnosticar, desativar. Observabilidade permanente deve depender principalmente de métricas, logs e traces com política de retenção.
Armadilhas comuns
Esquecer tokio_unstable
A TUI pode conectar sem entregar a visão esperada ou a aplicação pode não emitir a instrumentação detalhada. Force uma recompilação limpa quando estiver em dúvida:
cargo clean
RUSTFLAGS="--cfg tokio_unstable" cargo run
Use cargo clean com parcimônia em repositórios grandes; primeiro confira o comando efetivo e as flags.
Inicializar o subscriber tarde demais
Tasks criadas antes da inicialização podem não aparecer com o contexto desejado. Configure observabilidade no começo do main, antes de subir servidores e workers.
Confundir espera legítima com travamento
Consumers, listeners e timers passam grande parte da vida ociosos. Compare o estado observado com o contrato daquela task.
Usar std::sync::Mutex durante trabalho assíncrono longo
Um mutex síncrono pode ser correto para seções curtíssimas sem .await, mas segurá-lo durante espera ou trabalho longo bloqueia a thread do executor. Escolha a primitiva pelo comportamento, não por regra decorada.
Criar uma task por item sem limite
tokio::spawn é barato, não gratuito. Uma entrada mais rápida que a saída pode gerar milhares de tasks pendentes. Use semáforos, buffers limitados, buffer_unordered ou workers conforme o desenho.
Concluir que “Tokio está lento”
O runtime frequentemente só expõe um problema de aplicação: query sem índice, API externa lenta, lock amplo, CPU no executor ou pool saturado. O console aponta a espera; a causa pode estar fora do Tokio.
Colocar dados pessoais nos spans
A TUI é operacional, mas isso não elimina LGPD e políticas internas. Registre identificadores mínimos e evite payloads, tokens e informações pessoais.
Checklist de adoção
- instalar
tokio-consolecom versão controlada; - adicionar
console-subscriberao projeto; - habilitar
--cfg tokio_unstableno modo de diagnóstico; - inicializar observabilidade antes de criar tasks;
- compor a camada com o subscriber de logs existente;
- dar nomes e spans úteis às operações críticas;
- reproduzir contenção e tasks pendentes em ambiente controlado;
- restringir host, porta e acesso em produção;
- medir overhead sob carga representativa;
- remover dados sensíveis de campos de tracing;
- validar a correção com métricas e testes;
- revisar a integração ao atualizar Tokio.
tokio-console para carreira e portfólio Rust
Saber usar async/await é diferente de saber operar um serviço assíncrono. Em entrevistas para backend, plataforma e infraestrutura, uma boa explicação de incidente inclui:
- diferença entre task pendente e thread bloqueada;
- papel de
Poll, waker e scheduler; - contenção em
MutexeSemaphore; - backpressure em canais e streams;
- quando usar
spawn_blocking; - correlação entre tasks, spans e métricas;
- validação da correção por carga, não por intuição.
Um projeto de portfólio pode demonstrar isso com uma API Axum que possui limite de concorrência, tracing, uma rota de carga e um cenário reproduzível de lock amplo. Documente no README como iniciar a aplicação instrumentada e o que observar no console. Combine com Axum, Tokio, testes em Rust e CI/CD.
Para ampliar a comparação de modelos de concorrência, o conteúdo sobre concorrência com goroutines e channels em Go ajuda a separar runtime assíncrono, tasks cooperativas e threads do sistema sem transformar a discussão em “qual linguagem é mais rápida”.
Esse tipo de diagnóstico também é relevante ao buscar vagas Rust ou avaliar empresas que usam Rust: times maduros precisam de devs capazes de explicar latência e saturação, não apenas escrever handlers que compilam.
Perguntas frequentes
Para que serve o tokio-console?
Ele mostra o comportamento de tasks e recursos assíncronos instrumentados pelo Tokio. É útil para investigar espera excessiva, contenção, wakes frequentes, tasks antigas e trabalho que ocupa o executor por tempo demais.
Como conectar o console à aplicação?
Adicione console-subscriber, inicialize a camada no começo do processo, compile com RUSTFLAGS="--cfg tokio_unstable", execute a aplicação e rode tokio-console em outro terminal. Ajuste endereço e rede se a aplicação estiver em container ou host remoto.
Preciso usar Rust nightly?
Não necessariamente. tokio_unstable é um cfg de APIs do Tokio e não equivale, por si só, a exigir uma toolchain nightly. Ainda assim, a superfície não possui a mesma garantia de estabilidade das APIs normais e deve ser testada a cada atualização.
O console encontra deadlocks automaticamente?
Ele ajuda a visualizar tasks e recursos que não progridem, mas a interpretação continua sendo sua. Um conjunto de tasks aguardando locks pode ser deadlock, contenção esperada ou dependência lenta. Correlacione com o código e os spans.
Devo deixá-lo sempre ligado em produção?
Não como padrão irrefletido. Avalie overhead, exposição de dados e segurança de rede. Uma prática melhor é ativação controlada durante diagnóstico, mantendo métricas, logs e tracing como observabilidade contínua.
Conclusão
O tokio-console ocupa uma lacuna importante no ecossistema Rust: ele transforma o runtime assíncrono, normalmente invisível, em uma visão navegável de tasks, recursos, wakes e tempo de execução. Isso encurta a distância entre “a requisição pendurou” e “vinte tasks estão serializadas atrás deste lock mantido durante um timer”.
O fluxo recomendado é direto: instrumente com console-subscriber, compile o modo de diagnóstico com tokio_unstable, reproduza o sintoma, observe a relação entre tasks e recursos, corrija uma causa específica e valide com métricas. Não trate a TUI como prova final nem a exponha como painel público.
Continue a trilha com Tokio em profundidade, tracing vs log, OpenTelemetry em Rust, profiling em produção e background jobs com Tokio. Juntas, essas práticas ajudam a construir serviços assíncronos que não apenas funcionam, mas também podem ser explicados quando falham.