Kafka Producer: acks, retry e idempotência correta

1. Visão geral

Quando um producer envia uma mensagem para o Kafka, há três perguntas que você deve responder: enviar é o suficiente para ser considerado sucesso, se houver erro como fazer retry, e se o retry causa duplicação de mensagens. Este artigo explica acks, retries e enable.idempotence — três configurações que definem a confiabilidade e a correção do producer. Depois de ler, você saberá como configurar um producer “production-ready” e evitar erros comuns que causam perda ou duplicação de dados.

2. Dependências necessárias

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>3.7.0</version>
</dependency>

Os exemplos neste artigo usam o cliente Kafka puro (KafkaProducer), sem passar por Spring Kafka, para ver claramente como cada configuração afeta o comportamento.

3. acks: quando o producer considera que enviou com sucesso

acks determina quantos brokers o producer deve esperar confirmar antes de considerar a mensagem como enviada.

Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.ACKS_CONFIG, "all");
  • acks=0: o producer não aguarda nenhuma confirmação. Mais rápido, mas perde a mensagem se o broker cair logo após receber — nem mesmo sabendo que foi perdida.
  • acks=1: espera apenas o líder da partição escrever no log local e depois enviar ACK. Se o líder falhar antes do follower sincronizar, a mensagem é perdida.
  • acks=all (ou -1): espera que todas as réplicas no ISR (in-sync replica set) confirmem. Mais seguro, mas com latência maior.

Nota: acks=all é realmente seguro apenas quando acompanhado de min.insync.replicas >= 2 no broker. Se min.insync.replicas=1, acks=all também precisa apenas de 1 réplica confirmando — não é diferente de acks=1 em termos de segurança.

3.1. Configuração de broker que acompanha

# server.properties ou topic config
min.insync.replicas=2

Com replication factor = 3 e min.insync.replicas=2, o sistema consegue suportar 1 broker morrer e ainda conseguir escrever, enquanto ao mesmo tempo garante que os dados estejam em pelo menos 2 cópias antes do ACK.

4. retries: tratando erros temporários

O producer pode encontrar erros temporários como NotLeaderForPartitionException ou perda de conexão de rede por um curto período. retries permite que o producer resenda automaticamente sem precisar de manipulação manual de código.

props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 100);
props.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 120_000);
  • retries: número máximo de tentativas de retry. A partir do Kafka 2.1, deve-se definir bem alto (ou Integer.MAX_VALUE) e usar delivery.timeout.ms para controlar o tempo total de espera em vez de contar o número de vezes.
  • retry.backoff.ms: tempo de espera entre tentativas de retry, para evitar spam no broker.
  • delivery.timeout.ms: tempo total máximo desde a chamada de send() até receber o resultado (sucesso ou falha final). Deve ser maior que request.timeout.ms + linger.ms.

Problema: se não for cuidadoso, retry pode fazer a mensagem ser enviada duplicadamente ou ser enviada fora de ordem.

4.1. Por que o retry causa duplicação de mensagens

1. Producer envia um lote contendo a mensagem A
2. Broker escreve A com sucesso, envia ACK de volta
3. ACK é perdido na rede (network timeout)
4. Producer não recebe o ACK -> considera como erro -> faz retry
5. Broker recebe o pedido novamente -> escreve A pela segunda vez
=> Tópico tem 2 cópias da mensagem A

Esta é uma situação clássica: o broker já escreveu com sucesso mas o producer não sabe, então envia novamente. A idempotência na próxima seção resolve corretamente esse problema.

4.2. Por que o retry pode causar falta de ordem

Se max.in.flight.requests.per.connection > 1, o producer pode enviar múltiplos lotes em paralelo. O lote 2 pode ser escrito pelo broker antes do lote 1 se o lote 1 sofrer um erro e estiver fazendo retry.

max.in.flight.requests.per.connection=5

Com essa configuração sem idempotência, a ordem das mensagens na partição não é garantida quando há retry.

5. Idempotência: resolvendo a duplicação causada por retry

enable.idempotence=true ajuda o producer a evitar automaticamente escrever duplicatas, graças ao Kafka atribuir Producer ID (PID) e número de sequência para cada lote.

props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);

Ao habilitar idempotência, Kafka define automaticamente os seguintes valores (você não precisa configurar manualmente, mas deve saber):

acks = all
retries = Integer.MAX_VALUE (ou bem alto)
max.in.flight.requests.per.connection <= 5

5.1. Mecanismo de funcionamento

Cada sessão do producer recebe um PID único do broker. Cada mensagem enviada em uma partição tem um número de sequência crescente. O broker mantém o último número de sequência escrito para cada PID + partição.

Producer envia lote seq=5 para a partição 0
Broker: "a última sequência que mantive é 4, 5 é a próxima -> escreve, mantém seq=5"
Producer reenvia (por causa de retry) o lote seq=5
Broker: "seq=5 eu já escrevi -> ignora, retorna ACK de sucesso de qualquer forma"

Por isso, o retry não cria registros duplicados — o broker detecta e remove automaticamente duplicatas com base no número de sequência, sem precisar que o producer ou consumidor lidem com isso.

Nota: idempotência apenas garante sem duplicatas em uma única sessão do producer e dentro do escopo de uma partição. Se o producer reiniciar (um novo PID é atribuído), o mecanismo de dedup baseado no PID antigo não se aplica mais. Para obter exactly-once em múltiplas partições/tópicos, você precisa de transações (transactional.id) — que está fora do escopo da idempotência simples.

5.2. Exemplo de configuração de producer production-ready

public ProducerFactory() {
    Properties props = new Properties();
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
    props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

    // Habilitar idempotência -> define automaticamente acks=all, retries alto, in-flight <= 5
    props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
    props.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 120_000);

    this.producer = new KafkaProducer<>(props);
}

public void sendOrderEvent(String orderId, String payload) {
    ProducerRecord<String, String> record =
            new ProducerRecord<>("order-events", orderId, payload);

    producer.send(record, (metadata, exception) -> {
        if (exception != null) {
            log.error("Envio do pedido {} falhou", orderId, exception);
        } else {
            log.info("Pedido {} escrito na partição {} offset {}",
                    orderId, metadata.partition(), metadata.offset());
        }
    });
}
  • ENABLE_IDEMPOTENCE_CONFIG = true é a linha mais importante — ela puxa acks=all automaticamente.
  • Callback em send() fornece o resultado real: se exception != null, significa que já foi feito retry até delivery.timeout.ms e ainda assim houve erro — neste ponto você precisa lidar com a lógica de negócio (log, alerta, salvar em tabela de erros).

6. Verificando o comportamento com testes

Para ilustrar a parte de dedup, simulamos um MessageDeduplicator simples que simula o mecanismo de número de sequência em nível de aplicação (usado quando você não pode habilitar idempotência, por exemplo, sistemas legados):

public class MessageDeduplicator {

    private final Map<String, Long> lastSeenSequence = new ConcurrentHashMap<>();

    public boolean shouldWrite(String partitionKey, long sequence) {
        Long last = lastSeenSequence.get(partitionKey);
        if (last != null && sequence <= last) {
            return false; // duplicata, ignorar
        }
        lastSeenSequence.put(partitionKey, sequence);
        return true;
    }
}
class MessageDeduplicatorTest {

    @Test
    void shouldSkipDuplicateSequence() {
        MessageDeduplicator deduplicator = new MessageDeduplicator();

        assertThat(deduplicator.shouldWrite("order-1", 5)).isTrue();
        assertThat(deduplicator.shouldWrite("order-1", 5)).isFalse(); // retry com seq duplicado
        assertThat(deduplicator.shouldWrite("order-1", 6)).isTrue();
    }

    @Test
    void shouldAllowDifferentPartitionKeysIndependently() {
        MessageDeduplicator deduplicator = new MessageDeduplicator();

        assertThat(deduplicator.shouldWrite("order-1", 1)).isTrue();
        assertThat(deduplicator.shouldWrite("order-2", 1)).isTrue();
    }
}

O primeiro teste afirma que a mesma sequência enviada novamente pela segunda vez é ignorada — correto no espírito da idempotência do producer. O segundo teste afirma que dedup se aplica apenas dentro do escopo de uma key/partição, não afeta outras keys.

7. Tabela de resumo de configurações por necessidade

Necessidade acks enable.idempotence Risco
Log, métrica não importante 0 false Perde mensagens em caso de erro, mas throughput mais alto
Balancear velocidade/segurança 1 false Perde mensagens se o líder falhar antes do follower sincronizar
Dados de negócio importantes (pedidos, pagamentos) all true Latência maior, mas sem perda, sem duplicação por retry

8. Erros comuns

  • Definir acks=all mas esquecer min.insync.replicas: pensa que é seguro mas o broker apenas precisa de 1 ISR ainda assim ACK — não diferente de acks=1.
  • Escrever lógica de retry manualmente em paralelo com o retry padrão do cliente Kafka: facilmente causa duplicação dupla de envio porque ambas as camadas de retry estão funcionando.
  • Habilitar idempotência mas deixar max.in.flight.requests.per.connection > 5: Kafka rejeitará essa configuração assim que o producer for inicializado (ConfigException), porque excede o limite que idempotência suporta.
  • Confundir idempotência com exactly-once em todo o sistema: idempotência apenas protege producer-para-broker. Se o consumidor processa a mensagem e depois chama um side-effect (enviar email, debitar dinheiro) não idempotente, você ainda pode ter processamento duplicado do lado do consumidor.

Nota: não ative enable.idempotence=true se o sistema estiver rodando uma versão do Kafka mais antiga que 0.11 — esse recurso ainda não é suportado pelo broker e o producer falhará durante a inicialização.

9. Conclusão

acks, retries e enable.idempotence são três peças inseparáveis quando você precisa de um producer confiável. acks=all garante que os dados sejam replicados adequadamente antes de considerado bem-sucedido; retries ajuda a superar erros temporários; e idempotência garante que esse retry não cria registros duplicados. Com dados de negócio importantes, use os três juntos por padrão — o custo de latência é muito menor comparado com o custo de lidar com dados incorretos ou duplicados depois.

kafkaproduceracksidempotênciasistemas-distribuídos