- Nanit usaba AWS S3 en su pipeline de procesamiento de video para analizar el sueño de bebés, pero con miles de cargas por segundo, el costo de las solicitudes PutObject representaba la mayor parte del gasto total
- Además, por la retención mínima de 1 día en las reglas de Lifecycle de S3, tenían que pagar 24 horas de almacenamiento por videos que en realidad se procesaban en menos de 2 segundos
- Para resolverlo, construyeron N3, un sistema de almacenamiento en memoria basado en Rust, y dejaron S3 solo como buffer de desbordamiento
- N3 es totalmente compatible con el pipeline de procesamiento existente mediante SQS FIFO, manteniendo garantías estrictas de orden y confiabilidad
- Como resultado, lograron un ahorro de aproximadamente 500 mil dólares al año y, al mismo tiempo, una arquitectura simple y estable
Contexto
Resumen del pipeline de procesamiento de video
- Las cámaras de Nanit graban fragmentos de video, solicitan una URL prefirmada de S3 al Camera Service y luego hacen una carga directa a S3
- AWS Lambda publica la clave del objeto en una cola SQS FIFO (particionada por baby_uid), y los pods de procesamiento de video consumen desde SQS, descargan desde S3 y luego ejecutan la inferencia del estado de sueño
- Ventajas de esta configuración
- El aterrizaje en S3 + el encolado con SQS desacoplan la carga de la cámara del procesamiento de video, evitando la pérdida de video incluso durante mantenimiento o caídas temporales
- No hace falta gestionar directamente disponibilidad ni durabilidad gracias a S3
- SQS FIFO + group ID preserva el orden por bebé, y los nodos de procesamiento pueden seguir siendo en su mayoría sin estado
- Las reglas de Lifecycle de S3 se encargan de la recolección de basura, así que no hace falta rastrear los videos ya procesados
Por qué hacía falta un cambio
- El costo de PutObject dominaba: los videos eran objetos de vida corta que solo aterrizaban por unos pocos segundos para ser procesados, pero a escala de miles de cargas por segundo, el costo por solicitud por objeto era el principal impulsor del gasto
- Si aumentaban la frecuencia de fragmentación (mandando más fragmentos pequeños) para reducir latencia, el costo crecía linealmente porque cada fragmento adicional implicaba otra solicitud PutObject
- El almacenamiento se cobraba por duplicado: aunque el procesamiento terminaba en unos 2 segundos, las reglas de borrado de Lifecycle seguían cobrando aproximadamente 24 horas de almacenamiento
- Necesitaban un diseño que mantuviera la confiabilidad y el orden estricto, pero que evitara el costo por objeto en la ruta normal y minimizara el almacenamiento por el que se “paga por esperar”
Plan
-
Principios de diseño
- Simplicidad a través de la arquitectura: eliminar complejidad a nivel de diseño, no con implementaciones ingeniosas
- Corrección: un reemplazo completo y transparente para el resto del pipeline
- Optimizar para la ruta normal: diseñar para el caso común y usar S3 como red de seguridad en casos límite; como el algoritmo de procesamiento tolera huecos ocasionales, priorizar la simplicidad antes que construir garantías complejas
-
Factores que guiaron el diseño
- Objetos de vida corta: los segmentos existen en el área de aterrizaje solo durante unos segundos
- Orden: secuenciación estricta por bebé (sin procesar antes lo más reciente)
- Rendimiento: miles de cargas por segundo, con segmentos de 2 a 6 MB
- Limitaciones del cliente: las cámaras tienen reintentos limitados, así que no se puede asumir retransmisión
- Operación: había que tolerar millones de elementos en backlog durante mantenimiento o escalado
- Sin cambios de firmware: debía funcionar con las cámaras existentes
- Tolerancia a pérdida: se permiten huecos muy pequeños, y el algoritmo los enmascara
- Costo: evitar el costo por objeto de S3 en la ruta normal y minimizar el almacenamiento de “pagar por esperar”
Resumen del diseño (ruta normal de N3 + desbordamiento a S3)
-
Arquitectura
- N3 es una zona de aterrizaje personalizada que guarda el video en memoria solo durante el tiempo necesario para drenarlo al procesamiento (aprox. 2 segundos), y solo usa S3 cuando N3 no puede manejar la carga
- Dos componentes
- N3-Proxy (sin estado, doble interfaz)
- Externa (conectada a internet): acepta cargas de las cámaras mediante URL prefirmadas
- Interna (privada): emite URL prefirmadas al Camera Service
- N3-Storage (con estado, solo interno): guarda los segmentos cargados en RAM y los encola en SQS con URLs de descarga direccionables al pod
- Los pods de procesamiento de video consumen de SQS FIFO y descargan desde el almacenamiento al que apunte la URL (N3 o S3)
-
Flujo normal (Happy Path)
- La cámara solicita una URL de carga al Camera Service
- El Camera Service pide una URL prefirmada a la API interna de N3-Proxy
- La cámara sube el video al endpoint externo de N3-Proxy
- N3-Proxy lo reenvía a N3-Storage
- N3-Storage guarda el video en memoria y lo encola en SQS con una URL de descarga que apunta a sí mismo
- El pod de procesamiento descarga desde N3-Storage y procesa
-
Fallback de dos niveles
- Nivel 1: fallback a nivel proxy (por solicitud)
- Si N3-Storage no puede aceptar la carga por presión de memoria, backlog de procesamiento o falla del pod, N3-Proxy sube el video a S3 en nombre de la cámara
- La cámara ya había recibido una URL de N3 antes de detectar la falla
- Nivel 2: redirección a nivel clúster (todo el tráfico)
- Si N3-Proxy o N3-Storage están insanos, el Camera Service deja de emitir URLs de N3 y devuelve directamente URLs prefirmadas de S3
- Todo el tráfico va a S3 hasta que N3 se recupere
-
Por qué separarlo en dos componentes
- Radio de falla: si el almacenamiento cae, el proxy todavía puede enrutar a S3; si el proxy cae, solo se afecta el tráfico de ese nodo y no todo el clúster de almacenamiento
- Perfil de recursos: el proxy es intensivo en CPU/red (terminación TLS), mientras que el almacenamiento es intensivo en memoria (retención de video), con distintos tipos de instancia y necesidades de escalado
- Seguridad: el almacenamiento nunca toca internet
- Seguridad en despliegues: al actualizar el proxy (sin estado) no se toca el almacenamiento (que guarda datos activos)
Validación del diseño
-
Qué había que validar
- Capacidad y dimensionamiento: la duración real de las cargas en redes de clientes, el cómputo necesario y el tamaño del buffer de carga
- Modelo de almacenamiento: si todo podía mantenerse en RAM o si hacía falta disco
- Resiliencia: cómo hacer balanceo de carga barato y manejar nodos fallidos
- Políticas operativas: necesidades de GC, expectativas de reintentos y si borrar en GET era suficiente
- Desconocidos desconocidos: qué casos límite aparecerían cuando la idea se encontrara con la realidad
-
Enfoque 1: prueba sintética de estrés
- Construyeron un generador de carga para llevar el sistema al límite con distintas concurrencias, clientes lentos, carga sostenida y caídas en el procesamiento
- Objetivo: encontrar puntos de ruptura, detectar cuellos de botella inesperados y obtener una línea base determinista para planear capacidad
-
Enfoque 2: PoC en producción (modo espejo)
- Las pruebas sintéticas no podían reproducir el comportamiento real de las cámaras: Wi‑Fi inestable, distintas versiones de firmware y condiciones de red impredecibles
- Modo espejo: n3-proxy primero escribía en S3 (para conservar producción) y luego también escribía en el N3-Storage del PoC (conectado a un SQS canario y a un procesador de video)
- Cohortes objetivo: por versión de firmware / listas de Baby-UID
- Paridad de datos: comparar el estado de sueño entre el PoC y producción, e investigar diferencias
- Observabilidad: dashboards por ruta (N3 vs S3), profundidad de cola, latencia/RPS, presupuesto de errores y análisis de egreso
- Las feature flags (usando Unleash) fueron clave: permitían cambiar cohortes en tiempo real sin desplegar, probar segmentos pequeños (firmware viejo, cámaras con Wi‑Fi débil) y restaurar de inmediato si aparecía un problema
-
Lo que descubrieron
- Cuellos de botella: la terminación TLS consumía la mayor parte del CPU, y el burstable networking de AWS se estrangulaba al agotarse los créditos
- Almacenamiento solo en memoria era viable: la distribución real del tiempo de carga y la concurrencia confirmaron que el working set podía guardarse en RAM con margen de seguridad, sin necesidad de disco
- Overhead de timestamps TCP: alrededor del 85% del total de bytes transferidos eran tramas ACK; al desactivar timestamps TCP (
sysctl -w net.ipv4.tcp_timestamps=0) ahorraron 12 bytes por ACK
- Riesgo: al transferir muchos bytes en el mismo socket, el número de secuencia podría reiniciarse y corromper datos por una mala fusión de paquetes retrasados
- Mitigación: (1) un socket nuevo por carga, (2) reciclar sockets entre n3-proxy ↔ n3-storage después de aproximadamente 1 GB transferido
- Fuga de memoria: tras el lanzamiento inicial, la memoria de n3-proxy crecía de forma constante
- El perfilado con
jemalloc mostró crecimiento en los buffers BytesMut de hyper por conexión
- Algunas conexiones de clientes se quedaban congeladas durante la transferencia y no se limpiaban, dejando buffers vivos y haciendo crecer la memoria
- Arreglo: hacer que los sockets fueran de vida corta y aplicar límites de tiempo
- Desactivar keep-alive: cerrar la conexión inmediatamente después de cada carga
- Endurecer timeouts: configurar timeouts de headers/socket para terminar cargas congeladas y liberar buffers
Almacenamiento
-
Almacenamiento en memoria
- Empezaron por la ruta más simple: almacenamiento en memoria, evitando afinación de I/O y usando estructuras de datos intuitivas
- Guardaban videos con
Arc<DashMap<Ulid, Bytes>>; cada carga incrementaba bytes_used y cada descarga borraba el video y decrementaba ese contador
- Empezaban a rechazar cargas al llegar a aprox. 80% de capacidad para evitar OOM, y señalaban a n3-proxy que dejara de firmar URLs de carga
- Un manejador
control permitía pausar manualmente las cargas y la recolección de basura
-
Reinicio elegante
- Como el almacenamiento era solo en memoria, había que evitar perder datos en vuelo durante reinicios
- Proceso de reinicio elegante
- El pod recibe
SIGTERM (el StatefulSet hace rolling de uno en uno)
- El pod pasa a estado Not Ready y sale del Service (sin nuevas cargas)
- Sigue sirviendo descargas de videos ya cargados
- Cuando las descargas se detienen (sin lecturas recientes → el procesamiento se drenó)
- Espera a que terminen las solicitudes abiertas
- Reinicia y luego pasa al siguiente pod
- En operación normal, los pods se drenan en cuestión de segundos
-
GC
- Usaban dos mecanismos de limpieza
- Borrar al descargar: borraban el video justo después de descargarlo; el PoC confirmó cero redescargas, y como el procesador de video ya reintentaba internamente, no hacía falta retener datos ni rastrear estado de “procesado”
- GC por TTL para rezagados: borrar al descargar no cubría segmentos que el procesador saltaba (no se descargan → no se borran)
- Agregaron una GC por TTL ligera: escaneo periódico del DashMap en memoria y eliminación de elementos más viejos que un umbral configurable (por ejemplo, varias horas)
- Modo mantenimiento: durante caídas planeadas del procesamiento, podían pausar la GC mediante controles internos para evitar borrar videos mientras el consumo estuviera detenido
Conclusión
-
Principales resultados
- Usando S3 como buffer de fallback y N3 como zona de aterrizaje principal, lograron un ahorro de aproximadamente 500 mil dólares al año manteniendo el sistema simple y confiable
- Idea clave: la mayoría de las decisiones de “construir vs comprar” se enfocan en capacidades, pero a escala la economía cambia el cálculo
- Para objetos de vida corta (aprox. 2 segundos en operación normal), no hace falta replicación ni durabilidad sofisticada; un almacenamiento simple en memoria funciona
- Cuando el procesamiento se retrasa o el mantenimiento alarga la vida del objeto, sí hace falta la confiabilidad de S3
- Lo mejor de ambos mundos: N3 maneja eficientemente la ruta normal, y S3 aporta durabilidad cuando los objetos deben vivir más tiempo
- Si N3 tiene problemas (presión de memoria, caída de pods, problemas de clúster), las cargas conmutan de forma transparente a S3
-
Factores de éxito
- Definir claramente el problema desde el inicio: restricciones, supuestos y límites evitaron que el alcance creciera
- Validación temprana con PoC en modo espejo: permitió descubrir cuellos de botella (TLS, estrangulamiento de red) y validar supuestos antes de comprometerse
- Evitó sobreingeniería y retrocesos
-
Cuándo conviene construir algo así
- Vale la pena considerar infraestructura a medida cuando hay escala suficiente para generar un ahorro importante y cuando se cumplen restricciones específicas que permiten una solución simple
- El esfuerzo de ingeniería para construir y mantener el sistema debe ser menor que el costo de infraestructura que se elimina
- En el caso de Nanit, requisitos específicos (almacenamiento temporal, tolerancia a pérdida, fallback a S3) hicieron posible construir algo lo bastante simple como para mantener bajo el costo de mantenimiento
- Si no se cumplen ambos factores, es mejor seguir con servicios administrados
- ¿Lo volverían a hacer? Sí: el sistema corre estable en producción y el diseño con fallback permitió evitar complejidad sin sacrificar confiabilidad
3 comentarios
Me pregunto si no habría bastado con que el propio
ec2o un pod deeksrecibiera directamente la subida del video y la procesara.Si llegaron al punto de crear hasta un proxy, parecería que también habría sido totalmente posible hacer autoscaling de
ekssegún la carga de los pods.Normalmente, para procesar video no hace falta cargar el archivo completo en memoria; da la impresión de que, si hubieran creado archivos temporales en el SSD local de cada instancia y los hubieran procesado ahí, quizá ni siquiera habría hecho falta un fallback a
s3.Parece un ejemplo de mal uso de serverless y S3.
Pero la solución me parece aún más extraña.
Opiniones de Hacker News
Fue un artículo realmente útil. Me encanta que compartan este tipo de proceso de enfoque técnico
Aunque yo no haya pasado por exactamente el mismo problema, solo ver con qué forma de pensar lo abordaron ya deja mucho aprendizaje
Si soy sincero, esto probablemente habría sido mucho más limpio si no hubieran usado serverless desde el inicio
Da la impresión de que intentaron meter a la fuerza datos de unos pocos segundos dentro del paradigma serverless de AWS, y eso terminó creando costos y complejidad innecesarios
Aun así, moverlo a una solución basada en memoria fue una buena decisión
Dijeron que el TLS handshake consume bastante CPU, pero no parece que ese fuera el cuello de botella principal
Aun así, fue interesante ver que intentaran este tipo de diseño de sistemas ajustado al flujo de trabajo
En realidad, más que “implementar S3 por su cuenta” como dice el título, era una arquitectura con caché en memoria delante de S3
Está bueno, pero no es un reemplazo completo de S3 hecho en casa
Más allá del título, fue un proyecto interesante
Hablando en estilo HN, quiero comentar sobre la propia empresa Nanit
Nanit opera una cámara de monitoreo para bebés basada en la nube. Todo el video y el audio se sube sin E2EE
El hardware es caro y, sin suscripción, casi no sirve de mucho. Encima tienes que comprar un soporte de $200 para desbloquear la función de seguimiento del sueño
Es una lástima que este modelo termine reforzando una dependencia de la nube
Aun así, reducir la dependencia de S3 y moverlo a almacenamiento propio, como en este artículo, fue una buena decisión
Otros productos tenían apps inestables. Estaría bien una solución local-first + E2EE, pero en la práctica la usabilidad fue más importante
Si de verdad quieres E2EE, tendrías que hacer el análisis localmente y subir solo los resultados
Este artículo daba la sensación de que primero se crearon un problema y luego se felicitaron por resolverlo
Habría sido más simple y más barato vender desde el inicio hardware con almacenamiento local
El diseño centrado en la nube ya se siente como un enfoque de 2015
El artículo estuvo muy bien, pero también me dio curiosidad el ahorro de costos de haber implementado ‘delete on read’ en S3
Si S3 cobrara por segundo, el ahorro podría haber sido bastante grande
Además, esta solución en realidad se parece bastante a la opción de ‘reduced redundancy’ de S3
Dicen que ahorraron $500 mil, pero no sabemos cuál era el costo total
No es lo mismo si era $500,001 de $500 mil, o $500 mil de $55 millones
Da la impresión de que eligieron una arquitectura equivocada desde el principio y luego la maquillaron con caché
No hay razón para subir a S3 videos de 2 segundos en promedio, salvo para almacenamiento redundante
Si simplemente los hubieran procesado directo en el servidor, podrían haber eliminado S3, SQS y Lambda
No entiendo por qué hicieron tan complejo un problema tan simple
Parece la lección clásica de “concéntrate en desarrollar la app y simplifica la infraestructura”
Habría sido mejor meter la caché directamente dentro del servidor de procesamiento de video
El título habría sido más preciso si fuera “usamos mal S3”
Al final construyeron su propio almacenamiento en memoria, cuando podrían haber usado algo como Redis
Si el sistema que hicieron se cae, ¿se pierden los videos?
Habría sido mucho mejor enviarlo desde el principio a Kinesis o SQS