Lambda com filas do Kafka: paralelismo acima das partições, e o que ele custa
cerca de 6 min · áudio gerado ao tocar
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.
- Producers · unchanged
- Topic, 12 partitions · share group
- Event pollers · 10 invocations each
- Lambda function · idempotent handler
- DynamoDB · idempotency keys
- SQS · on-failure destination
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-offsetsó deduplica redelivery do broker. Retry do produtor gera offset novo para o mesmo pagamento; a chave tem de ser otransactionId, comattribute_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.msno 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: 2000com 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.
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.
Referências
Continue lendo
Deep dives de arquitetura, AWS, IA e mercado: direto no seu email. Grátis.
Sem spam · cancele quando quiser
Pergunte ao Fernando sobre isto
Uma resposta focada sobre este artigo, do meu assistente de IA, baseada no meu trabalho.
Participe da conversa
Entre para comentar
Confirme seu e-mail para participar, você também recebe a newsletter. Sem senha.