---
title: "tokio-console: Debug de Tasks Async em Rust | Rust Brasil"
url: "https://rustlang.com.br/blog/tokio-console-debug-tasks-async-rust-2026/"
markdown_url: "https://rustlang.com.br/blog/tokio-console-debug-tasks-async-rust-2026.MD"
description: "Use tokio-console para depurar tasks, locks, wakes e latência em aplicações Rust assíncronas. Guia com console-subscriber, Tokio unstable e produção segura."
date: "2026-08-14"
author: "Equipe Rust Brasil"
---

# tokio-console: Debug de Tasks Async em Rust | Rust Brasil

Use tokio-console para depurar tasks, locks, wakes e latência em aplicações Rust assíncronas. Guia com console-subscriber, Tokio unstable e produção segura.


**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](/artigos/tracing-vs-log/), 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 `Mutex` disputado?
- 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:

```text
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](/artigos/async-await-profundidade/) e o [guia completo de Tokio](/artigos/tokio-guia-completo/).

## Como o tokio-console funciona

A arquitetura tem duas partes:

1. **`console-subscriber` dentro da aplicação** recebe eventos de instrumentação e disponibiliza os dados de diagnóstico;
2. **`tokio-console` no 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:

```bash
--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:

```bash
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`:

```rust
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:

```bash
RUSTFLAGS="--cfg tokio_unstable" cargo run
```

Em outro terminal:

```bash
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 dos `spawn` relevantes;
- 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`:

```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:

```bash
#!/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](/blog/sccache-rust-cache-compilacao-ci-2026/).

## 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:

```rust
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:

```rust
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](/blog/rust-profiling-performance-producao-2026/) 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.

```rust
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](/artigos/logging-observabilidade/) e [OpenTelemetry em produção](/blog/rust-opentelemetry-producao-2026/).

## 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:

```rust
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](/blog/cargo-llvm-cov-cobertura-testes-rust-ci-2026/); para comportamento de memória, veja [Miri](/blog/miri-rust-undefined-behavior-2026/).

## Um roteiro de investigação de task travada

Quando o alerta é “requests penduram depois de alguns minutos”, siga um roteiro reproduzível:

1. **Defina o sintoma:** rota, percentil de latência, taxa de erro e janela de tempo.
2. **Reproduza em ambiente controlado:** carga, conjunto de features e configuração semelhantes.
3. **Ative o console:** de preferência por configuração explícita e acesso restrito.
4. **Filtre as tasks relevantes:** procure idade, estado, busy time e recursos aguardados.
5. **Correlacione com spans:** identifique request, worker ou dependência.
6. **Inspecione locks e limites:** `Mutex`, `RwLock`, `Semaphore`, pool de conexões e canais.
7. **Procure trabalho bloqueante:** chamadas síncronas, compressão, parsing grande, filesystem e CPU.
8. **Aplique uma alteração pequena:** reduza seção crítica, limite spawn ou mova CPU.
9. **Repita a carga:** compare console, métricas e latência.
10. **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:

```rust
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:

```bash
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-console` com versão controlada;
- [ ] adicionar `console-subscriber` ao projeto;
- [ ] habilitar `--cfg tokio_unstable` no 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 `Mutex` e `Semaphore`;
- 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](/ecossistema/axum/), [Tokio](/ecossistema/tokio/), [testes em Rust](/tutoriais/testes-rust/) e [CI/CD](/artigos/ci-cd-rust/).

Para ampliar a comparação de modelos de concorrência, o conteúdo sobre <a href="https://golang.com.br/aprenda/concorrencia-go/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">concorrência com goroutines e channels em Go</a> 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](/vagas/) ou avaliar [empresas que usam Rust](/empresas/): 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](/artigos/tokio-guia-completo/), [tracing vs log](/artigos/tracing-vs-log/), [OpenTelemetry em Rust](/blog/rust-opentelemetry-producao-2026/), [profiling em produção](/blog/rust-profiling-performance-producao-2026/) e [background jobs com Tokio](/blog/rust-background-jobs-filas-tokio-2026/). Juntas, essas práticas ajudam a construir serviços assíncronos que não apenas funcionam, mas também podem ser explicados quando falham.
