# Lambda com filas do Kafka: paralelismo acima das partições, e o que ele custa

O Lambda passou a consumir Kafka self-managed por share groups (KIP-932), com `ConsumptionMode: Queue`. Isso desacopla concorrência de número de partições, mas troca ordenação por vazão e move parte do controle de retry para o broker. Desmonto o padrão, mostro onde ele falha em silêncio e quando eu não usaria.

- URL: https://fernando.moretes.com/blog/lambda-com-filas-do-kafka-paralelismo-acima-das-particoes-e-o-que-ele

- Markdown: https://fernando.moretes.com/blog/lambda-com-filas-do-kafka-paralelismo-acima-das-particoes-e-o-que-ele/article.md?lang=pt

- Published: 2026-11-03T10:13:18.064Z

- Category: AWS & Cloud

- Tags: kafka, aws-lambda, event-driven, share-groups, finops, resiliencia

- Reading time: 6 min

- Source: [AWS Lambda now supports queues for Apache Kafka in self-managed Kafka event sources](https://aws.amazon.com/about-aws/whats-new/2026/11/aws-lambda-queue-kafka-esm/)

---

Durante anos a resposta para "meu consumidor Kafka não dá conta do pico" foi a mesma: mais partições. O problema é que partição não é um botão de vazão, é um contrato com o produtor, com o rebalanceamento e com quem opera o cluster. No dia 2 de novembro de 2026 a AWS colocou no event source mapping (ESM) de Kafka self-managed um segundo modo de consumo, `Queue`, construído sobre os share groups do Apache Kafka 4.2. A pergunta que importa não é "isso escala mais?". Escala. A pergunta é: quem garante que o registro 4.812 da partição 3 foi processado uma vez, e o que acontece com ele quando falha pela quinta vez às duas da manhã?

## O problema: partição virou teto de concorrência

No modo `Stream` (o padrão), o ESM usa o protocolo de consumer group: cada partição pertence a exatamente um consumidor. A documentação do Lambda é explícita sobre a consequência: em provisioned mode, para manter a ordem dentro da partição, o `MaximumPollers` é limitado ao número de partições do tópico, e cada poller sustenta até 5 MB/s ou até 5 invocações concorrentes.

Faça a conta para um tópico de 12 partições: 12 pollers vezes 5 invocações dá 60 execuções simultâneas, no máximo. Se o handler chama um modelo de fraude que leva 800 ms, o teto é algo perto de 75 registros por segundo por lote unitário, e não há `ReservedConcurrentExecutions` que mude isso.

As duas saídas tradicionais têm custo de manutenção, não de construção:

- **Superparticionar:** 12 vira 96. A chave de partição muda de distribuição, o rebalanceamento fica mais lento, o broker carrega mais arquivos de segmento e réplicas, e quase nunca se volta atrás, porque reduzir partição no Kafka não existe.
- **Copiar para outra fila:** um consumidor que republica em SQS. Agora você opera duas filas, duas políticas de retenção, dois pontos de duplicação e um código de ponte que alguém precisa manter atualizado.

Para trabalho em que cada evento é unidade independente (codificação de mídia, notificação, ingestão de telemetria, scoring de fraude por transação), pagar o preço da ordenação sem precisar dela é desperdício puro.

## Anatomia: share group, lock e acknowledgement

O KIP-932 troca a posse da partição por posse do registro. O broker mantém uma janela de registros em voo por partição, entre o share-partition start offset (SPSO) e o end offset (SPEO). Cada consumidor que busca dados adquire um lock temporário sobre registros, não sobre a partição. Ao final, o consumidor reconhece cada registro com um de três tipos: `ACCEPT` (processado), `RELEASE` (devolve para nova tentativa) ou `REJECT` (não processável, não volta).

Três parâmetros de grupo governam o comportamento, todos configuráveis por grupo com `kafka-configs.sh --entity-type groups` e limitados pelos mínimos e máximos do broker:

- `share.record.lock.duration.ms`: quanto tempo o lock vale. O padrão é 30 s; expirou, o registro volta a ficar disponível para outro consumidor.
- `share.delivery.count.limit`: quantas entregas um registro recebe antes de ser arquivado. O padrão é 5.
- `share.partition.max.record.locks`: quantos registros podem estar travados ao mesmo tempo por partição. É o teto real de paralelismo dentro de uma partição.

O Kafka 4.2, lançado em 17/02/2026, declarou os share groups prontos para produção e acrescentou o tipo `RENEW`, para estender o lock em processamento longo.

No Lambda, o ESM em Queue mode usa o Kafka Share Consumer: cada event poller lê de todas as partições, sem ordenação por partição, e sustenta até 10 invocações concorrentes, o dobro do modo Stream. O payload do evento continua idêntico ao do Kafka tradicional, com `topic`, `partition` e `offset`, então o código da função não muda.

## De onde vem o paralelismo, e para onde vai o registro que falha

O diagrama responde a uma pergunta: no Queue mode, quem controla concorrência e quem controla falha. A concorrência vem do número de pollers, não de partições. A falha tem dois caminhos: com destino de falha configurado, o Lambda envia o registro para SQS; sem ele, o broker refaz a entrega até o `share.delivery.count.limit` e depois arquiva. A seta tracejada é o caminho assíncrono de falha. Simplificação: rede VPC e autenticação do cluster ficaram de fora.

### Kafka 4.2 self-managed

- Producers unchanged (external)
- Topic, 12 partitions share group (messaging)

### Lambda ESM Queue mode

- Event pollers 10 invocations each (compute)
- Lambda function idempotent handler (compute)

### Side effects

- DynamoDB idempotency keys (data)
- SQS on-failure destination (messaging)

### Fluxos

- producers -> topic: publica
- pollers -> topic: adquire lock / ack
- pollers -> fn: invoca em lote
- fn -> ddb: escrita condicional
- pollers -> dlq: falha esgotada

## Stream e Queue no mesmo ESM
| Critério | Stream (padrão) | Queue |
| --- | --- | --- |
| Ordenação | Garantida por partição | Nenhuma |
| Invocações por poller | Até 5 | Até 10 |
| Teto de pollers (provisioned) | Número de partições | `MaximumPollers` até 2.000 |
| On-demand | Escala por offset lag | 5 pollers, 5 MB/s ou 50 invocações |
| Posição inicial | `StartingPosition` aceito | Não suportado; usa a posição do share group |
| Batching window | Suportado | Erro de validação |
| Amazon MSK | Sim | Não no lançamento |

## A configuração que decide se funciona

Ligar é uma linha: `SelfManagedKafkaEventSourceConfig.ConsumptionMode: Queue`. Operar é outra conversa. Quatro decisões fazem a diferença.

**Escala:** o on-demand do Queue mode para em 5 pollers, a 1 MB/s cada, ou seja 5 MB/s ou 50 invocações, o que vier primeiro. Para pico de verdade, provisioned mode: `MinimumPollers` de 1 a 200, `MaximumPollers` de 1 a 2.000, cada poller com até 5 MB/s. Na prática quem limita é a concorrência da função: 40 pollers vezes 10 são 400 execuções, e a cota padrão de concorrência da conta precisa comportar isso sem estrangular o resto.

**Lock versus timeout:** o lock padrão é 30 s. Se o lote de 100 registros leva 45 s na função, o broker considera o lock expirado e entrega os mesmos registros a outro poller. A documentação que li não diz se o poller renova o lock, então eu trato `share.record.lock.duration.ms` maior que o timeout da função como regra, e `BatchSize` pequeno o bastante para caber nele.

**Custo:** provisioned mode cobra por EPU-hora, US$ 0,185 em us-east-1 no exemplo da própria documentação, e um EPU comporta 10 pollers por padrão. ESMs em Queue só compartilham `PollerGroupName` com outros ESMs em Queue; misturar com Stream retorna erro de validação.

**Backlog:** não existe `StartingPosition`. Share group novo começa no offset mais recente, então um ESM criado sobre um tópico com fila acumulada ignora o acumulado até alguém ajustar `share.auto.offset.reset` no grupo.

> **O registro que some sem avisar:** No Queue mode, `MaximumRetryAttempts` tem padrão 0 e, sem destino de falha configurado, quem refaz a entrega é o broker. Depois de `share.delivery.count.limit` tentativas (5 por padrão), o registro é arquivado: não volta, não vai para fila nenhuma, não gera erro na função. Em notificação de cobrança ou alerta de fraude, isso é evento perdido sem trilha. Configure o destino de falha em SQS, SNS ou num tópico Kafka antes do primeiro deploy, não depois do primeiro incidente.

## Quando usar, e quando não

Use Queue mode quando três coisas forem verdade ao mesmo tempo: cada evento é unidade de trabalho independente, o handler é idempotente por uma chave de negócio (não pelo offset) e o gargalo medido é concorrência da função, não throughput do broker. Scoring de fraude por transação, envio de push e e-mail, thumbnail e transcodificação, enriquecimento de telemetria: todos passam nesse teste.

Não use quando o estado depende da sequência. Lançamento contábil por conta, máquina de estado de pedido, atualização de saldo, CDC que aplica `UPDATE` depois de `INSERT`: qualquer coisa em que "o evento 2 antes do evento 1" produz um número errado. Em sistema financeiro, isso não é bug de performance, é divergência de razão que alguém concilia na mão no fechamento.

Também não use como atalho para não entender o próprio lag. Se o consumidor está atrasado porque o handler faz uma chamada síncrona de 3 s a um sistema legado com 20 conexões no pool, dobrar a concorrência só move a fila do Kafka para o legado, e agora ele cai junto. O paralelismo novo precisa de um limite explícito na ponta: `ReservedConcurrentExecutions` na função ou `MaximumPollers` calculado a partir da capacidade de quem recebe a chamada.

E note o recorte do lançamento: só Kafka self-managed e Confluent Cloud, em versão 4.2 ou superior com share groups habilitados. Amazon MSK fica para quando oferecer essa versão.

## Anti-padrões

- **Idempotência pelo offset:** `topic-partition-offset` só deduplica redelivery do broker. Retry do produtor gera offset novo para o mesmo pagamento; a chave tem de ser o `transactionId`, com `attribute_not_exists(pk)` no DynamoDB e TTL alinhado à retenção do tópico.
- **Trocar o modo de um consumidor ordenado em produção:** mudar Stream para Queue num fluxo de saldo porque o lag subiu. A vazão melhora no mesmo dia e a conciliação quebra no fechamento do mês.
- **Lock menor que o timeout:** função com timeout de 120 s e `share.record.lock.duration.ms` no padrão de 30 s. Todo lote lento é entregue duas vezes, e o custo de invocação dobra exatamente no pico.
- **Paralelismo sem teto na ponta:** `MaximumPollers: 2000` com um downstream que aceita 50 conexões. O Kafka para de acumular e o banco começa a recusar.
- **Poller group misto:** planejar EPU compartilhado entre ESMs Stream e Queue. A API recusa, e o orçamento de provisioned mode calculado com a economia do agrupamento fica errado.

> **Migração sem big bang:** Share group e consumer group são independentes e leem o mesmo tópico sem interferir. Crie o ESM em Queue apontando para uma função de shadow que só grava a chave de idempotência e mede `Duration` e `ConcurrentExecutions`, ajuste `share.auto.offset.reset` se precisar do acumulado, compare por uma semana e só então desligue o ESM em Stream. Nenhum produtor precisa saber.

> **Nota do curador:** Já vi tópico passar de 24 para 192 partições por causa de um único consumidor lento, e essa decisão sobreviveu ao consumidor, ao time e à versão do cluster. O que eu faria hoje: Queue mode para o fluxo de notificação e scoring, destino de falha em SQS desde o primeiro dia, lock acima do timeout e um `MaximumPollers` calculado pela capacidade do downstream, não pelo pico do Kafka. Fluxo de saldo continua em Stream, com a partição que tem. A lição dura: o custo de superparticionar nunca foi o de criar as partições, foi o de conviver com elas por anos.

## Veredito

O Queue mode resolve um problema real: tira a partição do papel de teto de concorrência sem tocar em produtor, tópico ou código da função, e dobra as invocações por poller de 5 para 10. Dito isso, ele transfere responsabilidade: ordenação some, duplicação vira regra e, sem destino de falha, o broker arquiva o registro em silêncio depois de 5 entregas. **Adote** para trabalho independente e idempotente por chave de negócio, em Kafka 4.2 self-managed ou Confluent Cloud, com destino de falha e lock dimensionados antes do deploy. **Não adote** para qualquer fluxo em que a sequência define o estado; nesses, o Stream com partição bem escolhida continua sendo a resposta certa.

**Rating:** Adotar com condições / Adopt with condit

## Referências

- [AWS What's New: Lambda supports queues for Apache Kafka in self-managed Kafka event sources (02/11/2026)](https://aws.amazon.com/about-aws/whats-new/2026/11/aws-lambda-queue-kafka-esm/)
- [AWS Lambda Developer Guide: Polling Kafka topics in Queue mode](https://docs.aws.amazon.com/lambda/latest/dg/polling-kafka-queue.html)
- [AWS Lambda Developer Guide: Apache Kafka event poller scaling modes](https://docs.aws.amazon.com/lambda/latest/dg/kafka-scaling-modes.html)
- [AWS Lambda Developer Guide: Using Lambda with self-managed Apache Kafka](https://docs.aws.amazon.com/lambda/latest/dg/with-kafka.html)
- [KIP-932: Queues for Kafka](https://cwiki.apache.org/confluence/spaces/KAFKA/pages/255070434/KIP-932+Queues+for+Kafka)
- [Apache Kafka 4.2.0 Release Announcement](https://kafka.apache.org/blog/2026/02/17/apache-kafka-4.2.0-release-announcement/)
- [Confluent Platform: Share Groups](https://docs.confluent.io/platform/current/config-manage/kafka-queues.html)
