---
title: "Moka em Rust: Cache em Memória para Produção | Rust Brasil"
url: "https://rustlang.com.br/blog/moka-cache-rust-producao-2026/"
markdown_url: "https://rustlang.com.br/blog/moka-cache-rust-producao-2026.MD"
description: "Aprenda a usar Moka em Rust para cache concorrente: TTL, TTI, W-TinyLFU, get_with contra cache stampede, limites por peso, métricas e integração com Axum."
date: "2026-09-21"
author: "Equipe Rust Brasil"
---

# Moka em Rust: Cache em Memória para Produção | Rust Brasil

Aprenda a usar Moka em Rust para cache concorrente: TTL, TTI, W-TinyLFU, get_with contra cache stampede, limites por peso, métricas e integração com Axum.


**Use Moka quando uma aplicação Rust precisa reutilizar dados dentro do próprio processo com acesso concorrente, expiração e limite de memória — sem construir um cache manual com `HashMap` e locks.** Para backends com [Tokio](/ecossistema/tokio/) e [Axum](/ecossistema/axum/), a API `moka::future::Cache` oferece `get_with`, que também ajuda a impedir que muitas requisições recarreguem a mesma chave ao mesmo tempo. Moka não substitui Redis quando o cache precisa ser compartilhado entre réplicas; ele resolve a camada local, de latência muito baixa, que vive em cada instância.

Este guia monta um cache de produção, explica TTL, TTI, W-TinyLFU, peso, invalidação e observabilidade, e mostra como decidir entre Moka, Redis ou uma combinação dos dois.

## Resposta rápida: quando usar Moka

| Cenário | Escolha inicial | Motivo |
|---|---|---|
| Resultado caro reutilizado por requisições na mesma instância | **Moka** | acesso local, concorrente e sem round trip de rede |
| Dados precisam ser iguais e compartilhados entre réplicas | **Redis** | estado central acessível por vários processos |
| API recebe muito tráfego e consulta PostgreSQL | **Moka + Redis ou banco** | camada local absorve chaves quentes; origem continua autoritativa |
| Configuração pequena carregada raramente | `OnceLock`, `LazyLock` ou `Arc` | cache com política de remoção seria complexidade desnecessária |
| Cache simples, pequeno e sem concorrência | `HashMap` | a biblioteca padrão já resolve |
| Valores com tamanhos muito diferentes | **Moka com `weigher`** | capacidade por peso representa melhor o consumo |
| Cálculo assíncrono sujeito a rajadas na mesma chave | **Moka com `get_with`** | reduz inicializações concorrentes duplicadas |

A regra de bolso é: **Moka acelera uma instância; Redis conecta várias instâncias**. Se perder todas as entradas ao reiniciar o processo não é aceitável, o dado não pode existir apenas no Moka.

## O que é Moka e por que não usar apenas RwLock HashMap

Um cache real faz mais do que guardar pares chave-valor. Ele precisa decidir:

- quais entradas admitir quando a capacidade está pressionada;
- qual entrada remover;
- quando um valor fica velho;
- como impedir trabalho duplicado para a mesma chave;
- como atender muitas threads sem um lock global virar gargalo;
- como invalidar uma chave, um grupo ou todo o conjunto;
- como limitar custo quando um valor pesa 100 bytes e outro pesa 10 MB.

É possível começar com `Arc<RwLock<HashMap<K, V>>>`, mas logo surgem timers de expiração, filas de recência, contenção, limpeza e corridas entre `get` e `insert`. Moka entrega essas políticas em uma estrutura thread-safe com handles clonáveis.

A política de admissão e remoção é inspirada em **W-TinyLFU**. Em termos práticos, o cache considera recência e frequência para evitar que uma varredura de chaves usadas uma única vez expulse todo o conjunto quente. Isso costuma ser mais robusto que um LRU ingênuo para tráfego web, no qual endpoints podem receber tanto itens populares quanto long tails.

Moka oferece duas famílias principais:

- `moka::sync::Cache` para código síncrono e aplicações multithread;
- `moka::future::Cache` para aplicações assíncronas, como serviços Tokio.

Este artigo usa a API `future`, mais natural para backends modernos.

## Instalação e primeiro cache

Adicione a feature assíncrona no `Cargo.toml`:

```toml
[dependencies]
moka = { version = "0.12", features = ["future"] }
tokio = { version = "1", features = ["full"] }
```

Crie um cache limitado por quantidade e com expiração:

```rust
use std::time::Duration;
use moka::future::Cache;

#[derive(Clone, Debug)]
struct Perfil {
    nome: String,
    plano: String,
}

#[tokio::main]
async fn main() {
    let cache: Cache<u64, Perfil> = Cache::builder()
        .max_capacity(10_000)
        .time_to_live(Duration::from_secs(5 * 60))
        .time_to_idle(Duration::from_secs(60))
        .build();

    cache.insert(
        42,
        Perfil {
            nome: "Ferris".into(),
            plano: "pro".into(),
        },
    ).await;

    if let Some(perfil) = cache.get(&42).await {
        println!("{} usa o plano {}", perfil.nome, perfil.plano);
    }
}
```

`Cache` é barato de clonar: cada clone aponta para a mesma estrutura interna. Isso combina com o modelo de `State` do Axum e com serviços clonados por layers do [Tower](/ecossistema/tower/).

## TTL vs TTI: idade máxima e ociosidade

Os dois relógios respondem a problemas diferentes.

**TTL (time to live)** define quanto tempo uma entrada pode viver depois de inserida ou atualizada. Se o perfil de um usuário pode ficar até cinco minutos desatualizado, TTL de cinco minutos expressa esse orçamento de stale data.

**TTI (time to idle)** remove uma entrada depois de um período sem acesso. Ele ajuda a liberar capacidade de itens que já foram populares, mas deixaram de receber tráfego.

Ao combinar os dois:

- uma chave frequentemente acessada continua sujeita ao TTL e será atualizada periodicamente;
- uma chave abandonada pode sair antes, ao atingir o TTI;
- nenhum acesso prolonga indefinidamente um dado que precisa ser renovado.

Não escolha durações apenas por performance. O TTL é uma decisão de produto e consistência:

| Tipo de dado | TTL inicial razoável | Pergunta antes de definir |
|---|---:|---|
| feature flag operacional | segundos | quanto atraso de rollout é aceitável? |
| perfil público | minutos | edição precisa aparecer imediatamente? |
| catálogo quase estático | dezenas de minutos | existe invalidação por evento? |
| permissão/autorização | curto ou sem cache | revogação atrasada cria risco? |
| resposta de API externa cara | conforme contrato | o provedor informa validade? |

Para permissões, saldo, estoque ou qualquer dado sensível, trate invalidação e consistência como requisito. Cache não corrige um modelo sem fonte de verdade.

## Evitando cache stampede com get_with

O padrão ingênuo faz duas operações separadas:

```rust
if let Some(valor) = cache.get(&chave).await {
    return Ok(valor);
}

let valor = consultar_banco(&chave).await?;
cache.insert(chave, valor.clone()).await;
Ok(valor)
```

Sob uma rajada, cem requisições podem observar o miss antes que a primeira termine e disparar cem consultas iguais. Isso é **cache stampede** — o cache falha exatamente quando a chave fica mais quente.

Use `get_with` para inicialização por chave:

```rust
use std::sync::Arc;
use moka::future::Cache;

async fn buscar_perfil(
    cache: &Cache<u64, Arc<Perfil>>,
    usuario_id: u64,
) -> Arc<Perfil> {
    cache
        .get_with(usuario_id, async move {
            // Substitua pelo acesso real ao SQLx ou serviço remoto.
            Arc::new(carregar_perfil(usuario_id).await)
        })
        .await
}

async fn carregar_perfil(usuario_id: u64) -> Perfil {
    Perfil {
        nome: format!("usuario-{usuario_id}"),
        plano: "free".into(),
    }
}
```

Chamadas concorrentes para a mesma chave podem compartilhar a inicialização, enquanto chaves diferentes avançam em paralelo. O uso de `Arc<Perfil>` também evita copiar um valor grande a cada leitura.

`get_with` não resolve sozinho todos os picos. Em produção, combine-o com:

1. timeout na consulta à origem;
2. limite de conexões do banco;
3. backpressure ou limite de concorrência;
4. TTL com pequena variação aleatória quando muitas chaves são carregadas juntas;
5. retry apenas para erros transitórios e com orçamento total;
6. monitoramento de misses e latência da origem.

Para políticas transversais de timeout e concorrência em APIs, veja o guia de [tower-http com Axum](/blog/tower-http-middleware-producao-axum-2026/).

## Tratando erros sem cachear falhas acidentalmente

Nem todo carregamento produz um valor. Banco, rede e parsing podem falhar. Nesses casos, use a variante apropriada de inicialização que aceita resultado falível, como `try_get_with`, na versão adotada pelo projeto:

```rust
use std::sync::Arc;

async fn perfil_com_erro(
    cache: &moka::future::Cache<u64, Arc<Perfil>>,
    usuario_id: u64,
) -> Result<Arc<Perfil>, Arc<ErroRepositorio>> {
    cache
        .try_get_with(usuario_id, async move {
            repositorio_buscar(usuario_id).await.map(Arc::new)
        })
        .await
}
```

O objetivo é não transformar indisponibilidade temporária em um valor válido por minutos. Decida separadamente se um “não encontrado” deve ser cacheado. **Negative caching** é útil contra consultas repetidas a IDs inexistentes, mas pede TTL curto para não esconder um registro criado logo depois.

Uma abordagem clara é modelar o valor:

```rust
#[derive(Clone)]
enum PerfilCacheado {
    Encontrado(Arc<Perfil>),
    NaoEncontrado,
}
```

Assim, ausência esperada não é confundida com timeout, falha de conexão ou bug de deserialização.

## Limite por quantidade ou por peso

`max_capacity(10_000)` considera dez mil unidades. Sem `weigher`, cada entrada normalmente conta como uma unidade, seja ela minúscula ou enorme. Isso funciona para valores de tamanho parecido, mas pode mascarar consumo quando o cache guarda JSON, imagens, documentos ou vetores.

Use um peso estimado:

```rust
use std::{sync::Arc, time::Duration};
use moka::future::Cache;

#[derive(Clone)]
struct Documento {
    titulo: String,
    corpo: String,
}

let cache: Cache<String, Arc<Documento>> = Cache::builder()
    .max_capacity(128 * 1024 * 1024) // 128 MiB em peso aproximado
    .weigher(|chave: &String, doc: &Arc<Documento>| {
        let bytes = chave.len()
            + doc.titulo.len()
            + doc.corpo.len()
            + std::mem::size_of::<Documento>();

        u32::try_from(bytes).unwrap_or(u32::MAX)
    })
    .time_to_live(Duration::from_secs(300))
    .build();
```

O peso não mede heap com precisão absoluta: há overhead de alocadores, `Arc`, índices e metadados internos. Ele cria um orçamento proporcional, muito melhor do que fingir que todas as entradas custam o mesmo.

Valide o limite com métricas de processo. Se RSS continuar crescendo além do esperado, investigue valores retidos fora do cache, fragmentação, tarefas pendentes e outras estruturas.

## Integração com Axum

Coloque o cache no estado da aplicação:

```rust
use std::sync::Arc;
use axum::{
    extract::{Path, State},
    routing::get,
    Json, Router,
};
use moka::future::Cache;
use serde::Serialize;

#[derive(Clone)]
struct AppState {
    perfis: Cache<u64, Arc<PerfilResposta>>,
}

#[derive(Clone, Serialize)]
struct PerfilResposta {
    id: u64,
    nome: String,
}

async fn obter_perfil(
    State(state): State<AppState>,
    Path(id): Path<u64>,
) -> Json<PerfilResposta> {
    let perfil = state.perfis
        .get_with(id, async move {
            Arc::new(PerfilResposta {
                id,
                nome: format!("usuario-{id}"),
            })
        })
        .await;

    Json((*perfil).clone())
}

fn app() -> Router {
    let state = AppState {
        perfis: Cache::builder()
            .max_capacity(50_000)
            .build(),
    };

    Router::new()
        .route("/perfis/{id}", get(obter_perfil))
        .with_state(state)
}
```

Em um projeto real, mantenha a política de cache em uma camada de serviço ou repositório, não espalhada por todos os handlers. O handler deve pedir “obter perfil”; o serviço decide se lê Moka, Redis, [SQLx](/ecossistema/sqlx/) ou a origem.

Essa separação facilita testes e evita que detalhes como TTL virem regra implícita da API.

## Invalidação: por escrita, evento ou tempo

Expiração por tempo é a rede de segurança, não precisa ser a única estratégia. Quando a aplicação atualiza um perfil, pode invalidar a chave imediatamente:

```rust
async fn atualizar_perfil(
    cache: &Cache<u64, Arc<Perfil>>,
    usuario_id: u64,
    novo: Perfil,
) -> Result<(), ErroRepositorio> {
    salvar_no_banco(usuario_id, &novo).await?;
    cache.invalidate(&usuario_id).await;
    Ok(())
}
```

A ordem importa: grave primeiro na fonte de verdade; invalide depois. Se invalidar antes e a escrita falhar, você perde apenas performance. Se preencher o cache antes da transação confirmar, pode servir um estado que nunca existiu no banco.

Em várias réplicas, invalidar o Moka da instância A não remove a chave das instâncias B e C. Opções:

- TTL curto o bastante para o produto;
- evento em Redis Pub/Sub, NATS ou Kafka para invalidar caches locais;
- versionamento da chave (`perfil:{id}:{versao}`);
- não usar cache local para o dado que exige coerência imediata.

O padrão de duas camadas é comum:

1. **L1 Moka**, local e muito rápido;
2. **L2 Redis**, compartilhado entre instâncias;
3. banco ou serviço autoritativo como origem.

O guia de [Redis com Rust](/blog/rust-redis-cache-backend-2026/) cobre a camada distribuída. Não adicione L1 e L2 automaticamente: cada nível aumenta as combinações de invalidação e observabilidade.

## Observabilidade: hit rate sem se enganar

Um cache deve melhorar uma métrica de negócio técnico: latência, carga no banco, consumo de API externa ou custo de CPU. Registre pelo menos:

- hits e misses;
- latência de hit;
- latência de carregamento na origem;
- erros de carregamento;
- quantidade ou peso de entradas;
- invalidações por motivo;
- taxa de evicção;
- memória RSS do processo;
- p95/p99 do endpoint antes e depois.

Uma razão simples é:

```text
hit_rate = hits / (hits + misses)
```

Mas hit rate alto não prova sucesso. Um cache pode ter 99% de hits em dados baratos e ainda não reduzir a latência do endpoint caro. Segmente por operação, tipo de chave ou rota — sem colocar IDs individuais em labels, o que criaria cardinalidade explosiva.

Instrumente carregamentos com [tracing](/ecossistema/tracing/): um span para a operação lógica, campos como `cache.hit = true/false` e histogramas agregados no sistema de métricas. Nunca registre o valor cacheado se ele puder conter dados pessoais ou segredos.

## Moka vs Redis: a decisão completa

| Critério | Moka | Redis |
|---|---|---|
| Localização | memória do processo | serviço externo compartilhado |
| Latência | sem rede | inclui rede e serialização |
| Compartilhamento | somente a instância | várias instâncias e linguagens |
| Reinício | cache começa vazio | pode sobreviver conforme configuração |
| Tipos | valores Rust sem serialização obrigatória | bytes/estruturas do Redis |
| Coordenação distribuída | não | possível, com cuidado |
| Escala de memória | RAM de cada réplica | memória do cluster/servidor |
| Falha | afeta só aquela instância | pode afetar todos os consumidores |
| Melhor uso | L1 e memoização concorrente | L2, sessão, rate limit e dados compartilhados |

Escolha Moka quando o resultado pode ser recalculado e sua ausência inicial é aceitável. Escolha Redis quando compartilhar o dado faz parte do requisito. Use ambos somente quando o ganho medido justifica uma política de invalidação em duas camadas.

## Erros comuns em produção

### Cachear sem definir a fonte de verdade

Se ninguém sabe se o valor correto está no cache, no banco ou em outro serviço, a arquitetura já perdeu. Cache é derivado e descartável.

### Usar TTL longo para esconder origem lenta

Isso adia o incidente. Corrija índices, queries, pools, timeouts e limites. O cache deve reduzir carga saudável, não esconder uma origem quebrada.

### Capacidade por número para valores heterogêneos

Dez mil documentos não têm custo previsível. Use `weigher` e acompanhe RSS.

### Fazer get e insert separados sob concorrência

Esse padrão cria stampede. Use `get_with` ou `try_get_with` para inicialização coordenada por chave.

### Cachear erro transitório como resposta válida

Modele “não encontrado” separadamente e não transforme timeout em ausência.

### Esperar coerência entre réplicas

Cada processo possui seu próprio Moka. Invalidação local não é broadcast.

### Colocar dados secretos na chave ou em logs

Chaves podem aparecer em tracing e métricas. Prefira IDs internos ou hashes quando houver informação sensível.

### Testar apenas o caminho de hit

Teste miss simultâneo, expiração, invalidação depois de escrita, falha da origem, limite de capacidade e reinício com cache frio.

## Checklist de produção

1. Defina a fonte de verdade e confirme que o cache é descartável.
2. Escolha `moka::future::Cache` para Tokio e `moka::sync::Cache` para fluxo síncrono.
3. Use `get_with` ou `try_get_with` na inicialização concorrente.
4. Defina TTL pelo orçamento de desatualização do produto.
5. Adicione TTI se itens abandonados devem sair antes.
6. Use `weigher` quando valores tiverem custos diferentes.
7. Guarde valores grandes em `Arc` para evitar clones caros.
8. Invalide depois que a escrita autoritativa confirmar.
9. Planeje cache frio durante deploy e autoscaling.
10. Meça hits, misses, latência da origem, evicções e RSS.
11. Teste rajadas na mesma chave e em chaves diferentes.
12. Documente por que Moka é suficiente ou por que existe também Redis.

## Moka como projeto de portfólio e competência de carreira

Cache é um bom tema de portfólio porque expõe decisões que aparecem em sistemas reais: consistência, concorrência, memória, backpressure e observabilidade. Em vez de mostrar apenas um endpoint rápido, publique um benchmark reproduzível com:

- API Axum + banco;
- carga com cache desligado e ligado;
- rajada de misses na mesma chave;
- gráfico de p50/p95/p99;
- consultas por segundo no banco;
- política de TTL e justificativa;
- teste de invalidação após update;
- limite por peso e memória do processo.

Esse repositório conversa diretamente com vagas de backend, plataforma e sistemas distribuídos. Para conectar o estudo ao mercado, veja [vagas Rust](/vagas/), [empresas que usam Rust](/empresas/) e o roadmap de [carreira backend em Rust](/carreira/nicho-web-backend/).

## Perguntas frequentes

### O que é Moka em Rust?

Moka é um cache concorrente em memória para Rust, com APIs síncrona e assíncrona, expiração, invalidação, capacidade e política W-TinyLFU. Ele evita que cada projeto precise implementar locks, limpeza e remoção do zero.

### Moka substitui Redis?

Não quando os dados precisam ser compartilhados entre processos. Moka é local à instância; Redis é acessado por várias réplicas. Moka pode ser a camada L1 na frente de Redis, desde que a complexidade de invalidação seja justificada.

### Como evitar cache stampede com Moka?

Use `get_with` ou `try_get_with` para coordenar a inicialização da mesma chave. Acrescente timeout, limite de concorrência na origem e jitter se muitas entradas puderem expirar juntas.

### Qual a diferença entre TTL e TTI?

TTL limita a idade desde a inserção ou atualização. TTI limita o tempo sem acesso. Juntos, eles renovam dados populares e removem cedo os abandonados.

### Como limitar memória?

Para valores semelhantes, limite por quantidade pode bastar. Para valores heterogêneos, configure `weigher` com bytes estimados e acompanhe RSS; o peso é orçamento aproximado, não medição exata do heap.

### Moka funciona com Axum e Tokio?

Sim. Guarde `moka::future::Cache` no `State` ou em um serviço compartilhado. Clonar o cache cria outro handle para a mesma estrutura, não uma cópia completa dos valores.

## Conclusão

Moka é a escolha prática para um **cache local concorrente em Rust**: oferece política de admissão robusta, TTL, TTI, capacidade por peso e inicialização coordenada sem obrigar a equipe a manter uma estrutura caseira. Em Axum, comece com `moka::future::Cache`, `get_with`, TTL baseado no requisito de consistência e métricas da origem.

A decisão mais importante não é a duração exata do TTL. É saber se o dado pode ficar local, se pode desaparecer no restart e como será invalidado depois de uma escrita. Se as respostas exigirem compartilhamento entre réplicas, use Redis ou eventos; se não exigirem, Moka entrega uma camada L1 rápida, type-safe e alinhada ao modelo concorrente de Rust.
