# Busca Escalável de Usuários com Amazon Cognito: Análise Profunda

O Amazon Cognito é excelente para autenticação, mas sua API de listagem de usuários não foi projetada para buscas de alta frequência em grandes user pools. Neste artigo, analiso como construir uma camada de busca escalável sobre o Cognito, os modos de falha que surgem quando você ignora os limites da API nativa, e os trade-offs reais entre consistência eventual, privacidade de dados e custo operacional.

- URL: https://fernando.moretes.com/blog/busca-de-usuarios-em-cognito-com-escala-e-privacidade

- Markdown: https://fernando.moretes.com/blog/busca-de-usuarios-em-cognito-com-escala-e-privacidade/article.md?lang=pt

- Published: 2026-06-03T00:00:00.000Z

- Category: Segurança & Resiliência

- Tags: cognito, opensearch, identity, privacy, api-gateway, lambda, event-driven, financial-grade

- Reading time: 8 min

- Source: [Scalable user search with Amazon Cognito](https://aws.amazon.com/blogs/architecture/)

---

Todo arquiteto que já colocou um Amazon Cognito User Pool em produção com mais de 100 mil usuários conhece o momento exato em que a API ListUsers começa a decepcionar: throttling silencioso, paginação forçada, ausência de full-text search e nenhuma garantia de latência sub-segundo. O problema não é um bug — é um limite de design intencional. O Cognito foi construído para ser um plano de controle de identidade, não um motor de busca. Quando times de produto exigem autocomplete de usuários, busca por atributos customizados ou listagens filtradas em tempo real, a solução correta não é forçar a API nativa além de seus limites: é projetar uma camada de busca dedicada, sincronizada de forma assíncrona, com privacidade e consistência como cidadãos de primeira classe.

## Por que a API Nativa do Cognito Não Escala para Busca

O `ListUsers` do Cognito aceita um parâmetro `Filter` com sintaxe proprietária e suporta apenas correspondência por prefixo em atributos indexados — `email`, `phone_number`, `cognito:user_status` e poucos outros. Não há suporte a busca fuzzy, relevância por score, busca em atributos customizados (`custom:*`) ou ordenação por campos arbitrários. O limite de taxa padrão para `ListUsers` é **5 RPS por User Pool** na maioria das regiões, com burst limitado. Em um sistema financeiro com dezenas de microsserviços chamando esse endpoint — onboarding, KYC, suporte ao cliente, backoffice — esse teto é atingido em segundos.

Além do throttling, há o problema de latência estrutural. Cada chamada `ListUsers` com paginação (`PaginationToken`) é sequencial; não é possível paralelizar a varredura de um pool de 500 mil usuários. O tempo para iterar sobre o pool completo pode chegar a minutos, o que torna inviável qualquer caso de uso que exija resposta em tempo real.

O terceiro vetor de problema é a privacidade. Retornar atributos de usuário diretamente de uma API de busca — mesmo interna — sem filtragem de campos expõe PII (nome, CPF, data de nascimento) a consumidores que talvez precisem apenas do `sub` (identificador único). Em ambientes regulados por LGPD ou PCI-DSS, isso é um risco de conformidade concreto, não teórico.

## Arquitetura de Busca Escalável sobre Cognito

Fluxo completo: eventos de ciclo de vida do Cognito disparam sincronização assíncrona para OpenSearch, enquanto a API de busca serve leituras com projeção de campos controlada por IAM.

### 👤 Identity Plane — Amazon Cognito

- Cognito User Pool Lifecycle triggers (security)
- Post-Confirmation & Post-Auth Lambda (compute)

### ⚡ Event Bus — Async Sync

- EventBridge Custom Bus (messaging)
- SQS DLQ max 3 retries (messaging)
- Sync Lambda Idempotent upsert (compute)

### 🔍 Search Plane — OpenSearch

- OpenSearch users-v2 index (data)
- KMS CMK Encryption at rest (security)

### 🌐 API Layer — Search Entrypoint

- API Gateway REST + Authorizer (edge)
- Search Lambda Field projection (compute)
- WAF Rate limit + IP rules (security)

### 📊 Observability

- CloudWatch SLO dashboards (external)

### Fluxos

- userpool -> lambda_trigger: Trigger pós-confirmação
- lambda_trigger -> eventbridge: Publica evento user.created/updated
- eventbridge -> sync_lambda: Regra de roteamento
- eventbridge -> dlq: Falha após retries
- sync_lambda -> opensearch: Upsert idempotente por sub
- opensearch -> kms: Chave CMK
- waf -> apigw: Inspeção de tráfego
- apigw -> search_lambda: JWT validado
- search_lambda -> opensearch: Query DSL + _source filtering
- sync_lambda -> cloudwatch: Métricas de lag
- search_lambda -> cloudwatch: Latência p99

## Como o Pipeline de Sincronização Realmente Funciona

O mecanismo central é um pipeline de sincronização assíncrona orientado a eventos. O Cognito expõe Lambda Triggers para eventos de ciclo de vida: `PostConfirmation`, `PostAuthentication`, `PreTokenGeneration` e, para deleção, um trigger via `AdminDeleteUser` que pode ser interceptado com um Lambda customizado antes da chamada SDK. Cada um desses triggers publica um evento no EventBridge com um schema versionado — `{ "source": "identity.cognito", "detail-type": "user.created" }` — contendo apenas o `sub` do usuário e os atributos não-sensíveis necessários para indexação.

A Lambda de sincronização consome esses eventos e executa um **upsert idempotente** no OpenSearch usando o `sub` como `_id` do documento. Idempotência aqui não é opcional: o EventBridge garante entrega *at-least-once*, e um evento duplicado sem idempotência resulta em re-indexação desnecessária com risco de inconsistência de versão. A solução é usar o campo `_seq_no` e `_primary_term` do OpenSearch para controle otimista de concorrência, ou simplesmente aceitar que um upsert por `_id` é naturalmente idempotente para o caso de uso de busca.

Para a carga inicial — migrar um pool existente de N usuários — o padrão é um job de backfill que usa `ListUsers` com paginação, mas executado **uma única vez**, fora do caminho crítico, com rate limiting explícito (máximo 4 RPS para ficar abaixo do limite de 5 RPS) e checkpoint em DynamoDB para retomada em caso de falha. Esse job não precisa ser rápido; precisa ser correto e retomável.

> **Consistência Eventual Não é Fraqueza — é um Contrato:** A janela de inconsistência entre o Cognito e o índice OpenSearch é tipicamente de 200ms a 2s em condições normais, mas pode chegar a minutos se a Lambda de sincronização estiver throttled ou o EventBridge estiver sob carga. Para casos de uso de busca (autocomplete, listagem de backoffice), essa janela é aceitável. Para casos de uso de autorização (verificar se um usuário existe antes de emitir um token), o Cognito é a fonte de verdade — nunca o índice de busca. Documentar esse contrato explicitamente no ADR do sistema evita que times de produto construam dependências incorretas sobre a camada de busca.

## Privacidade por Design: Projeção de Campos e Minimização de Dados

O erro mais comum que vejo em implementações de busca sobre dados de identidade é indexar tudo e filtrar na apresentação. Isso viola o princípio de minimização de dados da LGPD (Art. 6º, III) e cria um índice que, se comprometido, expõe o perfil completo de cada usuário. A abordagem correta é indexar apenas os campos necessários para busca e retornar apenas os campos necessários para o consumidor.

No OpenSearch, isso se traduz em duas decisões de design distintas. Primeiro, o **mapeamento do índice** deve omitir campos sensíveis como CPF, data de nascimento e número de telefone completo — esses campos não devem existir no índice. Se um atributo não está no mapeamento, ele não pode ser vazado. Segundo, a **projeção de resposta** via `_source` filtering no DSL da query deve ser aplicada pela Search Lambda com base no escopo do token JWT do chamador. Um token com escopo `search:basic` recebe apenas `{ sub, display_name, email_prefix }`; um token com escopo `search:admin` recebe campos adicionais como `account_status` e `created_at`.

A Lambda de sincronização também deve aplicar hashing ou truncamento antes de indexar. Por exemplo, indexar `email_domain` (parte após o @) em vez do email completo permite buscas por domínio corporativo sem expor endereços individuais. Para busca por nome, técnicas como n-gram tokenization no OpenSearch permitem autocomplete sem armazenar o nome completo em texto claro — embora isso aumente o tamanho do índice em 3-4x e deva ser avaliado contra o custo de armazenamento do cluster.

## Números Reais: Cognito Nativo vs. Camada de Busca Dedicada

- **5 RPS** — Limite de ListUsers por User Pool. Teto nativo; burst não documentado e inconsistente entre regiões
- **<50ms** — Latência p99 de busca via OpenSearch. Com índice bem mapeado e instância r6g.large.search, carga de 500 RPS
- **~$180/mês** — Custo base de cluster OpenSearch (1 nó r6g.large). Para pools de até 2M usuários com índice otimizado; escala horizontal com 3 nós para HA

## Modos de Falha que Ninguém Documenta

**Drift silencioso entre Cognito e OpenSearch** é o modo de falha mais insidioso. Se a Lambda de sincronização falhar silenciosamente — por exemplo, um erro de permissão IAM após uma rotação de role — o índice envelhece sem alarme visível. A mitigação é um **reconciliation job** periódico (diário ou semanal) que compara contagens e amostras aleatórias entre o Cognito e o OpenSearch, publicando uma métrica `search.index.drift_count` no CloudWatch com alarme em qualquer valor > 0 por mais de 1 hora.

**Explosão de índice por atributos customizados dinâmicos** é outro vetor. O Cognito permite até 50 atributos customizados por User Pool. Se a Lambda de sincronização indexar todos eles sem um mapeamento explícito, o OpenSearch usará dynamic mapping e criará campos para cada variação, levando a um *mapping explosion* que pode derrubar o cluster. A solução é sempre definir um mapeamento explícito com `dynamic: false` e uma allowlist de campos indexáveis.

**Throttling em cascata durante backfill** é um risco de operação. Se o backfill inicial e o pipeline de sincronização em tempo real competirem pelo mesmo pool de workers Lambda, o backfill pode esgotar a concorrência reservada e atrasar eventos de ciclo de vida reais. A solução é executar o backfill em uma função Lambda separada com concorrência reservada isolada e uma fila SQS dedicada com batch size conservador (10-20 mensagens).

**Vazamento de PII via query logging** é frequentemente ignorado. O OpenSearch pode logar queries completas no CloudWatch Logs, incluindo termos de busca que podem conter nomes ou fragmentos de email. Em ambientes regulados, o slow log e o audit log devem ser configurados com mascaramento de campos ou desabilitados para campos sensíveis.

## Anti-Padrões que Custam Caro em Produção

- Chamar `ListUsers` em loop no caminho crítico de uma API de produto — latência imprevisível, throttling garantido sob carga e sem SLA de latência.
- Usar o índice de busca como fonte de verdade para decisões de autorização — o índice é eventualmente consistente; um usuário deletado pode aparecer como ativo por segundos ou minutos.
- Indexar o objeto completo do usuário Cognito (incluindo `custom:*` attributes) sem mapeamento explícito — mapping explosion, aumento de custo de armazenamento e superfície de exposição de PII.
- Sincronizar via polling periódico do Cognito em vez de triggers de ciclo de vida — janela de inconsistência proporcional ao intervalo de polling, custo de ListUsers desnecessário e ausência de granularidade por evento.
- Expor o endpoint OpenSearch diretamente via API Gateway sem uma Lambda de projeção — impossível aplicar field-level security baseado em escopo de token sem uma camada de lógica intermediária.
- Omitir o reconciliation job periódico — drift silencioso entre Cognito e o índice se torna invisível até que um incidente de suporte ao cliente o revele.

## Opções de Backend de Busca: OpenSearch vs. DynamoDB vs. RDS
| Critério | Critério | OpenSearch | DynamoDB + GSI | RDS Aurora (ILIKE) |
| --- | --- | --- | --- | --- |
| Full-text / Fuzzy | Nativo (BM25, n-gram) | Não suportado | Limitado (ILIKE, pg_trgm) | — |
| Latência p99 @ 500 RPS | < 50ms | < 10ms (chave exata) | 50-200ms (sem índice otimizado) | — |
| Custo para 2M usuários | ~$180-360/mês (1-3 nós) | ~$30-80/mês (WCU/RCU + storage) | ~$200-500/mês (instância + storage) | — |
| Field-level security nativa | Sim (OpenSearch Security plugin) | Não (requer lógica na aplicação) | Não (requer lógica na aplicação) | — |
| Complexidade operacional | Alta (cluster, snapshots, upgrades) | Baixa (serverless) | Média (RDS managed, mas schema migrations) | — |

## Segurança e IAM: Zero Trust na Camada de Busca

Em um ambiente financeiro, a camada de busca de usuários é um alvo de alto valor: qualquer vazamento de dados aqui pode resultar em multas regulatórias e danos reputacionais. O modelo de segurança deve seguir o princípio de menor privilégio em cada hop.

A **Search Lambda** deve ter uma role IAM com permissão apenas para `es:ESHttpGet` e `es:ESHttpPost` no ARN específico do domínio OpenSearch, com uma condição `aws:SourceVpc` para garantir que chamadas só ocorram dentro da VPC. O acesso ao OpenSearch deve ser configurado com **Fine-Grained Access Control** habilitado, mapeando a role IAM da Lambda a um papel OpenSearch com permissões de leitura apenas no índice `users-v2`.

A **Sync Lambda** precisa de `es:ESHttpPut` e `es:ESHttpDelete` no mesmo domínio, mas deve ser uma role separada — nunca a mesma role da Search Lambda. Separar as roles de leitura e escrita limita o raio de explosão se uma das funções for comprometida.

O domínio OpenSearch deve ser implantado **dentro de uma VPC privada**, sem endpoint público, com Security Groups restringindo acesso apenas às Lambdas relevantes. O KMS CMK para criptografia em repouso deve ter uma key policy que permita apenas as roles das Lambdas e administradores explicitamente nomeados — sem `kms:*` para `Principal: "*"`.

Para auditoria, o CloudTrail deve estar habilitado com data events para o domínio OpenSearch, e o Access Log do API Gateway deve ser enviado para um bucket S3 com Object Lock habilitado (modo COMPLIANCE, retenção de 90 dias para PCI-DSS) para garantir imutabilidade dos logs de acesso.

## Avaliação pelos Pilares do AWS Well-Architected

- **security**: Field-level security via OpenSearch Fine-Grained Access Control; roles IAM separadas para leitura e escrita; KMS CMK com key policy restritiva; VPC-only endpoint; CloudTrail com data events; API Gateway com WAF e rate limiting por IP e por token.
- **reliability**: EventBridge com DLQ para eventos de sincronização não processados; reconciliation job periódico para detecção de drift; backfill com checkpoint em DynamoDB para retomada; OpenSearch com 3 nós em múltiplas AZs para HA; alarmes CloudWatch em `search.index.drift_count` e latência p99.
- **performance**: Índice OpenSearch com mapeamento explícito e n-gram tokenizer para autocomplete; Lambda de busca com Provisioned Concurrency para eliminar cold start em horários de pico; API Gateway com cache de resposta para queries frequentes (TTL 30s para listagens de backoffice).
- **cost**: OpenSearch Serverless como alternativa para workloads intermitentes (custo por OCU-hora vs. instância reservada); Lambda com ARM (Graviton2) reduz custo de compute em ~20%; S3 Intelligent-Tiering para snapshots do OpenSearch; revisão mensal de índices não utilizados.

> **Nota do Arquiteto: O que eu Faria Diferente:** Em produção, o erro que mais me custou foi não documentar o contrato de consistência eventual desde o dia zero — times de produto construíram lógica de negócio assumindo que o índice de busca era síncrono com o Cognito, e o resultado foi bugs sutis em fluxos de onboarding. Hoje, a primeira coisa que faço é escrever o ADR com o SLO de lag máximo aceitável (ex: 99,9% dos eventos indexados em < 60s) e expor uma métrica `search.sync.lag_seconds` no dashboard de produto, não só no dashboard de infra. A segunda lição: nunca use OpenSearch Serverless para um pool de usuários com mais de 500k registros sem antes modelar o custo de OCU — a cobrança por OCU-hora pode surpreender em cargas de trabalho com queries de alta frequência. Para a maioria dos casos financeiros que vi, um cluster de 3 nós `r6g.large.search` com Reserved Instances de 1 ano é mais previsível e 40-60% mais barato que Serverless no steady state.

## Veredicto: Construa a Camada de Busca, Não Contorne o Cognito

O Amazon Cognito é uma escolha sólida para gerenciamento de identidade em sistemas financeiros — mas sua API de listagem não é um motor de busca e nunca será. Tentar contornar seus limites com polling agressivo, caching de curta duração ou queries paralelas é uma corrida contra o throttling que você vai perder em escala. A arquitetura correta é aceitar o Cognito como plano de controle de identidade e construir uma camada de busca dedicada — baseada em OpenSearch, sincronizada via eventos de ciclo de vida, com privacidade por design e consistência eventual documentada como contrato explícito. O custo incremental (um cluster OpenSearch de ~$180-360/mês) é trivial comparado ao custo de um incidente de throttling em produção ou de uma auditoria de privacidade que encontra PII desnecessária indexada. Invista no pipeline de sincronização correto, documente o SLO de lag, e trate o índice de busca como o que ele é: uma projeção de leitura otimizada, não uma fonte de verdade.

**Rating:** Strongly Recommended with Caveats

## Referências e Leitura Adicional

- [Amazon Cognito — ListUsers API Reference](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ListUsers.html)
- [Amazon OpenSearch Service — Fine-Grained Access Control](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/fgac.html)
- [Amazon Cognito — Lambda Trigger Overview](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html)
- [Amazon EventBridge — Event Delivery and Retries](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rule-dlq.html)
- [AWS Well-Architected Framework — Security Pillar](https://docs.aws.amazon.com/wellarchitected/latest/security-pillar/welcome.html)
- [OpenSearch — Index Mapping and Dynamic Templates](https://opensearch.org/docs/latest/field-types/index/)
- [LGPD — Lei Geral de Proteção de Dados Pessoais (Art. 6º)](https://www.planalto.gov.br/ccivil_03/_ato2015-2018/2018/lei/l13709.htm)
- [AWS Architecture Blog — Scalable user search with Amazon Cognito](https://aws.amazon.com/blogs/architecture/)
