- A Nanit usava o AWS S3 em um pipeline de processamento de vídeo para analisar o sono de bebês, mas com milhares de uploads por segundo, o custo das requisições PutObject passou a representar a maior parte do custo total
- Além disso, por causa da retenção mínima de 1 dia das regras de Lifecycle do S3, era necessário pagar por 24 horas de armazenamento para vídeos que, na prática, eram processados em cerca de 2 segundos
- Para resolver isso, a empresa construiu o N3, um sistema de armazenamento em memória baseado em Rust, e passou a usar o S3 apenas como buffer de overflow
- O N3 é totalmente compatível com o pipeline de processamento existente por meio do SQS FIFO, mantendo garantia estrita de ordem e confiabilidade
- Como resultado, a empresa obteve uma economia anual de cerca de US$ 500 mil, além de uma arquitetura simples e estável
Contexto
Visão geral do pipeline de processamento de vídeo
- A arquitetura era tal que as câmeras da Nanit gravavam chunks de vídeo, solicitavam uma URL pré-assinada do S3 ao Camera Service e faziam upload direto para o S3
- O AWS Lambda publicava a chave do objeto em uma fila SQS FIFO (com shard por baby_uid), e os pods de processamento de vídeo consumiam do SQS, baixavam do S3 e executavam a inferência do estado de sono
- Vantagens dessa configuração
- O pouso no S3 + o enfileiramento no SQS desacoplavam o upload da câmera do processamento de vídeo, evitando perda de vídeos mesmo durante manutenção ou indisponibilidade temporária
- Não era necessário gerenciar diretamente disponibilidade e durabilidade por conta do S3
- Com SQS FIFO + group ID, a ordem por bebê era preservada, e os nós de processamento podiam permanecer em sua maioria stateless
- As regras de Lifecycle do S3 cuidavam da coleta de lixo, dispensando rastrear vídeos já processados
Por que era necessário mudar
- O custo do PutObject era dominante: os vídeos eram objetos de vida curta que ficavam na área de pouso por apenas alguns segundos até serem processados, mas em uma escala de milhares de uploads por segundo, o custo por requisição de objeto tornou-se o principal fator de custo
- Se a empresa aumentasse a frequência de chunking (enviando mais chunks menores) para reduzir a latência, o custo cresceria linearmente, porque cada chunk adicional significaria outra requisição PutObject
- O armazenamento era uma segunda cobrança: mesmo quando o processamento terminava em cerca de 2 segundos, as regras de exclusão do Lifecycle ainda implicavam aproximadamente 24 horas de custo de armazenamento
- Era necessário um desenho que mantivesse confiabilidade e garantia estrita de ordem, mas evitasse o custo por objeto no caminho normal e minimizasse o armazenamento pelo qual se “paga para esperar”
Plano
-
Princípios de projeto
- Simplicidade por arquitetura: remover complexidade no nível do design, e não com implementações espertas
- Correção: ser um substituto completo e transparente para o restante do pipeline
- Otimizado para o caminho normal: projetar para o caso comum e usar o S3 como rede de segurança para edge cases; como o algoritmo de processamento tolera lacunas ocasionais, a simplicidade teve prioridade sobre a construção de garantias complexas
-
Fatores que guiaram o design
- Objetos de vida curta: os segmentos existem na área de pouso por apenas alguns segundos
- Ordem: sequenciamento estrito por bebê (sem processar o mais recente primeiro)
- Vazão: milhares de uploads por segundo, com 2–6 MB por segmento
- Limitações do cliente: as câmeras têm número limitado de tentativas de retry, então não se pode assumir reenvio
- Operação: era necessário tolerar backlogs de milhões de itens durante manutenção ou scale-up
- Sem mudança de firmware: precisava funcionar com as câmeras existentes
- Tolerância a perda: lacunas muito pequenas são aceitáveis, e o algoritmo as mascara
- Custo: evitar o custo por objeto do S3 no caminho normal e minimizar armazenamento de “pagar para esperar”
Visão geral do design (caminho normal do N3 + overflow para o S3)
-
Arquitetura
- O N3 é uma área de pouso customizada que mantém vídeo em memória apenas pelo tempo necessário para o processamento drenar (cerca de 2 segundos), usando o S3 somente quando o N3 não consegue absorver a carga
- Dois componentes
- N3-Proxy (stateless, interface dupla)
- Externo (conectado à internet): aceita uploads das câmeras por meio de URLs pré-assinadas
- Interno (privado): emite URLs pré-assinadas para o Camera Service
- N3-Storage (stateful, somente interno): armazena os segmentos enviados em RAM e os coloca em uma fila SQS com uma URL de download endereçável ao pod
- Os pods de processamento de vídeo consomem do SQS FIFO e fazem download do armazenamento indicado pela URL (N3 ou S3)
-
Fluxo normal (Happy Path)
- A câmera solicita uma URL de upload ao Camera Service
- O Camera Service solicita uma URL pré-assinada à API interna do N3-Proxy
- A câmera envia o vídeo para o endpoint externo do N3-Proxy
- O N3-Proxy encaminha o upload ao N3-Storage
- O N3-Storage mantém o vídeo em memória e o coloca no SQS com uma URL de download apontando para si mesmo
- O pod de processamento baixa do N3-Storage e processa
-
Fallback em duas camadas
- Camada 1: fallback no nível do proxy (por requisição)
- Se o N3-Storage não conseguir receber o upload por pressão de memória, backlog de processamento, falha de pod etc., o N3-Proxy faz o upload para o S3 em nome da câmera
- A câmera já havia recebido uma URL do N3 antes que a falha fosse detectada
- Camada 2: redirecionamento no nível do cluster (todo o tráfego)
- Se o N3-Proxy ou o N3-Storage estiverem não saudáveis, o Camera Service para de emitir URLs do N3 e retorna diretamente uma URL pré-assinada do S3
- Todo o tráfego passa a ir para o S3 até que o N3 se recupere
-
Por que separar em dois componentes
- Raio de falha: se o storage cair, o proxy ainda consegue rotear para o S3; se o proxy cair, apenas o tráfego daquele nó é afetado, e o cluster de storage como um todo continua intacto
- Perfil de recursos: o proxy é intensivo em CPU/rede (terminação TLS), enquanto o storage é intensivo em memória (retenção de vídeo), com tipos de instância e requisitos de escala diferentes
- Segurança: o storage nunca entra em contato com a internet
- Segurança de rollout: ao atualizar o proxy (stateless), o storage (que mantém dados ativos) não é tocado
Validação do design
-
O que precisava ser validado
- Capacidade e dimensionamento: a duração real dos uploads em diferentes redes de clientes, além do compute necessário e do tamanho do buffer de upload
- Modelo de armazenamento: se era possível manter tudo em RAM ou se seria necessário disco
- Resiliência: como fazer load balancing barato e lidar com nós com falha
- Políticas operacionais: exigências de GC, expectativa de retries e se excluir no GET seria suficiente
- Desconhecidos desconhecidos: quais edge cases surgiriam quando a ideia encontrasse a realidade
-
Abordagem 1: teste sintético de estresse
- Foi construído um gerador de carga para empurrar o sistema até o limite, com diferentes níveis de concorrência, clientes lentos, carga contínua e indisponibilidade do processamento
- Objetivo: encontrar pontos de quebra, identificar gargalos inesperados e obter uma linha de base determinística para planejamento de capacidade
-
Abordagem 2: PoC em produção (modo espelho)
- Testes sintéticos não conseguiam reproduzir o comportamento real das câmeras: Wi‑Fi instável, versões variadas de firmware e condições de rede imprevisíveis
- Modo espelho: o n3-proxy primeiro gravava no S3 (preservando produção) e depois também gravava no N3-Storage do PoC (ligado a um SQS canário e ao processador de vídeo)
- Coorte alvo: por versão de firmware / lista de Baby-UIDs
- Paridade de dados: comparação do estado de sono entre o PoC e a produção, investigando divergências
- Observabilidade: dashboards por caminho (N3 vs S3), profundidade de fila, latência/RPS, orçamento de erro e análise de egress
- Feature flags (com Unleash) foram essenciais: permitiam trocar coortes em tempo real sem deploy, testar fatias estreitas (firmware antigo, câmeras com Wi‑Fi fraco) e reverter imediatamente em caso de problema
-
O que foi descoberto
- Gargalos: a terminação TLS consumia a maior parte da CPU, e o burstable networking da AWS sofria throttling após esgotar créditos
- Storage só em memória era viável: pela distribuição real do tempo de upload e da concorrência, confirmou-se que era possível manter o working set em RAM com folga de segurança, sem precisar de disco
- Overhead de TCP timestamps: cerca de 85% do total de bytes transmitidos eram frames ACK; ao desativar TCP timestamps (
sysctl -w net.ipv4.tcp_timestamps=0), houve economia de 12 bytes por ACK
- Risco: em transmissões com alto volume de bytes no mesmo socket, poderia haver wrap do número de sequência e corrupção por remontagem incorreta de pacotes atrasados
- Mitigação: (1) novo socket por upload, (2) reciclar sockets entre n3-proxy ↔ n3-storage após cerca de 1 GB transferido
- Vazamento de memória: após o lançamento inicial, a memória do n3-proxy crescia continuamente
- O profiling com
jemalloc mostrou crescimento nos buffers hyper BytesMut por conexão
- Algumas conexões de clientes paravam no meio da transferência e não eram limpas, deixando buffers acumulados e aumentando a memória continuamente
- Correção: tornar os sockets de curta duração e aplicar limites de tempo
- Keep-alive desativado: cada conexão era encerrada logo após a conclusão do upload
- Timeouts mais rígidos: timeouts de header/socket passaram a encerrar uploads travados e liberar buffers
Armazenamento
-
Armazenamento em memória
- A equipe começou pelo caminho mais simples: armazenamento em memória, evitando tuning de I/O e usando estruturas de dados intuitivas
- Os vídeos eram armazenados em
Arc<DashMap<Ulid, Bytes>>; cada upload aumentava bytes_used, e cada download apagava o vídeo e decrementava esse valor
- Ao atingir cerca de 80% ou mais da capacidade, o sistema começava a recusar uploads para evitar OOM, sinalizando ao n3-proxy que parasse de assinar URLs de upload
- Um handle
control permitia pausar manualmente uploads e garbage collection
-
Reinício gracioso
- Como o storage era somente em memória, era necessário evitar perda de dados em andamento durante reinícios
- Processo de reinício gracioso
- O pod recebia
SIGTERM (com o StatefulSet fazendo rolling um de cada vez)
- O pod entrava em estado Not Ready e saía do Service (sem novos uploads)
- Continuava servindo downloads para vídeos já enviados
- Quando os downloads paravam (sem leituras recentes → drenagem do processamento)
- Aguardava a conclusão das requisições abertas
- Depois reiniciava e passava para o próximo pod
- Em operação normal, os pods drenavam em poucos segundos
-
GC
- Foram usados dois mecanismos de limpeza
- Excluir ao baixar: o vídeo era removido logo após o download; no PoC, confirmou-se zero redownloads, e como o processador de vídeo já fazia retries internamente, não era necessário manter dados nem rastrear estado de “processado”
- TTL GC para os que ficaram para trás: excluir no download não cobria segmentos que o processador pulava (não baixados → não excluídos)
- Foi adicionado um TTL GC leve: varredura periódica do DashMap em memória e remoção de itens mais antigos que um limiar configurável (por exemplo, algumas horas)
- Modo de manutenção: durante indisponibilidades planejadas do processamento, era possível pausar o GC via controle interno, evitando exclusão de vídeos enquanto o consumo estivesse parado
Conclusão
-
Principais resultados
- Usando o S3 como buffer de fallback e o N3 como área principal de pouso, a empresa alcançou economia de cerca de US$ 500 mil por ano, mantendo o sistema simples e confiável
- Insight central: a maioria das decisões de “construir vs comprar” foca em funcionalidade, mas na escala, a economia muda a conta
- Para objetos de vida curta (cerca de 2 segundos em operação normal), não era necessária replicação nem durabilidade sofisticada; um armazenamento simples em memória funcionava
- Quando o processamento atrasava ou a manutenção prolongava a vida dos objetos, eram necessárias as garantias de confiabilidade do S3
- Melhor dos dois mundos: o N3 tratava o caminho normal com eficiência, enquanto o S3 fornecia durabilidade quando os objetos precisavam viver mais tempo
- Se algo desse errado no N3 (pressão de memória, crash de pod, problema no cluster), os uploads faziam failover de forma transparente para o S3
-
Fatores de sucesso
- Definir claramente o problema antes: restrições, premissas e limites evitaram expansão de escopo
- Validar cedo com um PoC em modo espelho: isso revelou gargalos (TLS, throttling de rede) e validou premissas antes de comprometer a solução
- Evitando overengineering e retrabalho
-
Quando vale construir algo assim
- Vale considerar infraestrutura customizada quando há escala suficiente para gerar economia relevante e quando restrições específicas permitem uma solução simples
- O esforço de engenharia para construir e manter o sistema precisa ser menor do que o custo de infraestrutura eliminado
- No caso da Nanit, requisitos específicos (armazenamento temporário, tolerância a perda, fallback para S3) permitiram construir algo simples o bastante para manter baixo o custo de manutenção
- Sem esses dois fatores, o melhor é ficar com serviços gerenciados
- Fariam de novo? Sim, porque o sistema roda de forma estável em produção e o design com fallback permite evitar complexidade sem sacrificar confiabilidade
3 comentários
Fico me perguntando se não daria para simplesmente o próprio EC2 ou os pods do EKS receberem o upload dos vídeos e processarem diretamente
Se chegaram ao ponto de criar até um proxy, parece que daria bem para lidar também com o auto scaling do EKS conforme a carga dos pods
No processamento de vídeo, normalmente não é preciso carregar o arquivo inteiro em memória; se tivessem criado arquivos temporários no SSD local de cada instância e processado por ali, dá a sensação de que nem precisariam de fallback para o S3
Parece um exemplo de uso errado de serverless e do S3.
Mas a solução também parece ainda mais estranha.
Comentários do Hacker News
Foi um texto realmente muito útil. Gosto muito quando compartilham esse processo de abordagem técnica
Mesmo sem eu passar exatamente pelo mesmo problema, só de ver com que linha de raciocínio chegaram à solução já dá para aprender bastante
Sinceramente, isso provavelmente teria ficado muito mais limpo se não tivessem usado serverless desde o começo
Parece que tentaram encaixar à força dados de poucos segundos no paradigma serverless da AWS, e isso acabou gerando custo e complexidade desnecessários
Ainda assim, migrar para uma solução baseada em memória foi uma boa escolha
Disseram que o handshake TLS consumia bastante CPU, mas não parece ser o principal gargalo
Mesmo assim, foi interessante ver essa tentativa de projetar um sistema sob medida para o workflow
Na prática, mais do que “implementar o S3 por conta própria”, isso era uma arquitetura com cache em memória na frente do S3
É bacana, mas não chega a ser um substituto completo do S3
Independentemente do título, foi um projeto interessante
Falando no estilo HN, quero comentar sobre a própria empresa, a Nanit
A Nanit opera uma câmera de monitoramento de bebês baseada em nuvem. Todo o vídeo e áudio é enviado sem E2EE
O hardware é caro e, sem assinatura, mal dá para usar. Além disso, é preciso comprar um suporte de $200 para liberar o recurso de rastreamento do sono
É uma pena que essa estrutura acabe reforçando esse modelo de dependência da nuvem
Ainda assim, foi uma boa decisão reduzir a dependência do S3 e migrar para armazenamento próprio, como neste texto
Outros produtos tinham apps instáveis. Seria bom ter uma solução local-first + E2EE, mas na prática a usabilidade era mais importante
Se alguém quiser E2EE de verdade, a análise teria de ser feita localmente, com apenas os resultados sendo enviados
O texto passou uma sensação de comemorar por ter resolvido um problema que a própria empresa criou
Teria sido mais simples e barato vender hardware com armazenamento local desde o início
Esse tipo de design centrado em nuvem já parece uma abordagem de 2015
O texto foi ótimo, mas também fiquei curioso sobre a economia de custos ao implementar ‘delete on read’ no S3
Se o S3 fosse cobrado em intervalos de segundos, a economia poderia ter sido considerável
Além disso, essa solução na prática lembra a opção de ‘reduced redundancy’ do S3
Dizem que economizaram $500 mil, mas não sabemos qual era o custo total
Faz diferença se foi $500.001 em um total de $500 mil ou $500 mil em um total de $55 milhões
Parece que escolheram uma arquitetura errada desde o início e depois tentaram remendar com cache
Não há motivo para enviar vídeos de 2 segundos em média para o S3, além de armazenamento duplicado
Se tivessem processado isso diretamente no servidor, poderiam ter eliminado S3, SQS e Lambda de uma vez
Não entendo por que transformaram um problema tão simples em algo tão complicado
Parece aquela lição clássica de “foque no desenvolvimento do app e mantenha a infraestrutura simples”
Teria sido melhor colocar o cache diretamente dentro do servidor de processamento de vídeo
Um título mais preciso talvez fosse “usamos o S3 do jeito errado”
No fim, construíram um armazenamento em memória próprio, quando poderiam simplesmente ter usado algo como Redis
Se o sistema deles cair, os vídeos simplesmente desaparecem?
Se tivessem enviado isso para o Kinesis ou SQS desde o começo, teria sido muito melhor