- Una startup que acababa de activar la monetización sufrió una falla en los pagos de suscripción, pero como el problema no se reproducía internamente, la identificación de la causa se retrasó 5 días
- La falla comenzó al copiar un patrón de conversión de Prisma/TypeScript→Python/SQLAlchemy generado por ChatGPT, donde en lugar de una función para generar UUID se había puesto una cadena de ID hardcodeada como si fuera el valor por defecto
- Debido a una estructura de 8 tareas de AWS ECS con 5 instancias cada una, los usuarios podían encontrarse con uno de hasta 40 pools de IDs únicos distintos, y durante el día los despliegues frecuentes ocultaban el problema
- Por la noche, al detenerse los despliegues, el único ID de cada servidor se agotaba, y después los nuevos intentos de suscripción fallaban por colisiones de ID únicos
- Con 50 quejas al día, durante 5 días y una suscripción mensual de $40, la pérdida estimada fue de $10,000 al mes; la falta de pruebas, logging y alertas, además de copiar código, agravó la respuesta al incidente
Falla de suscripción revelada justo después de monetizar
- La startup activó por primera vez la monetización en mayo y consiguió a su primer cliente dentro de la primera hora del lanzamiento
- A la mañana siguiente, en Gmail se habían acumulado más de 40 quejas de usuarios
- Los usuarios no podían completar la suscripción
- Reportaban que al presionar el botón de suscripción aparecía un spinner de carga infinito
- Crearon una cuenta nueva para comprobarlo directamente, pero internamente la suscripción funcionaba bien y no lograban reproducir la causa
- Durante el horario laboral casi no había quejas, y la falla se acumulaba sobre todo por la noche
Implementación de monetización bajo presión de tiempo
- Mayo coincidía con el inicio de la batch YC S23, y el equipo aún no tenía claro cuál era la mejor dirección tras el lanzamiento
- Dalton, group partner de YC, aconsejó usar a los suscriptores de pago como métrica para definir el rumbo y subir al doble el precio mensual que tenían en mente
- El precio final quedó en $40 al mes
- El proyecto originalmente era full-stack en NextJS, pero antes y después del trabajo de monetización avanzaban en una migración a Python/FastAPI
- Usaron ChatGPT durante la migración
- También completaron la integración con Stripe
- Durante los 5 días siguientes durmieron mucho menos y tuvieron que atender entre 30 y 50 correos de queja cada día
El patrón de conversión de modelos generado por ChatGPT
- Durante la migración del backend trasladaron los modelos de base de datos de Prisma/TypeScript a Python/SQLAlchemy
- Como convertir los modelos era tedioso y consideraron que ChatGPT lo hacía bien, lo usaron en casi toda la migración
- El código generado se copiaba y pegaba, luego se verificaba que funcionara, y como en producción parecía operar con normalidad, siguieron adelante
- En ese momento las inserciones en la base de datos seguían a cargo de la API de Next, y el backend en Python solo leía la base de datos
- Al implementar la suscripción empezaron por primera vez a insertar registros en la BD desde Python
- El nuevo modelo de SQLAlchemy lo hicieron manualmente, pero copiaron tal cual el patrón generado por ChatGPT de los modelos existentes
- El mismo problema quedó en la forma de generar IDs de todos los modelos
La causa real y por qué no se veía de día
- El error clave fue pasar una cadena de ID hardcodeada en lugar de pasar una función o lambda que generara el UUID
- Cuando un usuario completaba la suscripción con ese ID en una instancia concreta del backend, los intentos posteriores de suscripción en esa misma instancia provocaban una colisión de ID único
- La configuración del backend ocultó el problema por más tiempo
- Operaban 8 tareas de ECS en AWS
- Cada tarea ejecutaba 5 instancias del backend
- Los usuarios podían terminar potencialmente en uno de 40 IDs distintos
- Durante el día hacían entre 10 y 20 commits directos al branch
main, y cada uno disparaba un nuevo despliegue del backend- Cada vez que ocurría un despliegue, aparecían 40 IDs nuevos disponibles para los clientes
- Por la noche se detenían los commits y despliegues, así que el único ID de cada servidor se agotaba rápidamente
- Al principio había cerca de 40 servidores capaces de aceptar suscripciones, pero con el tiempo ese número se acercaba a 0
Magnitud de la pérdida y medidas posteriores
- La pérdida se estimó como
50 emails/day x 5 days x $40/month, es decir, $10,000 de ingresos mensuales- Ese cálculo toma en cuenta solo a los usuarios que enviaron una queja
- Para encontrar la causa hicieron falta 5 días, muchísimos correos, cientos de logs de Sentry, una larga conversación en Discord con ingenieros de Stripe y revisar cinco archivos clave
- Después de descubrir la causa, Adam subió rápidamente la corrección
- Luego añadieron pruebas unitarias y de integración sólidas, alertas y logging
- Este incidente muestra que cuando se combinan error humano, pruebas insuficientes, copia de código y pushes directos a
main, incluso una sola línea pequeña puede terminar causando una gran pérdida de ingresos
2 comentarios
Eh, el código generado automáticamente por IA siempre hay que revisarlo; ¿por qué lo usan tal cual?
Opiniones de Hacker News
La falta de monitoreo fue lo que les hizo perder 10 mil dólares. La app estaba generando excepciones de base de datos de forma continua y masiva, pero nadie recibió una alerta.
Si hubieran tenido esa alerta, habría sido una investigación de 5 minutos, no de 5 días. Si no arreglaron el sistema de alertas, en realidad no arreglaron nada.
Programar es fácil cuando todo funciona bien; lo difícil es la parte que maneja los problemas.
Desde el momento en que entran clientes de pago, se necesita a alguien con el conocimiento y la experiencia para encargarse de logging, monitoreo, alertas, seguridad, etc. No se puede tratar DevOps como amateur.
Pero lo realmente absurdo es que no haya logging de errores y alertas en la base de datos. No es código legacy de hace 20 años, sino un producto nuevo, y tampoco es código de la época en que se usaban errores de DB como validación de datos.
La publicación del blog da 404, así que dejo el enlace de Web Archive.
https://web.archive.org/web/20240610032818/https://asim.bear...
El autor agregó una corrección importante: dice que las prácticas aquí eran muy malas y vergonzosas, y que después añadieron pruebas unitarias/de integración sólidas y alertas/logging. En resumen, fue un error humano y, visto en retrospectiva, algo claramente evitable.
También agregó que ocurrió durante las primeras semanas de la empresa, bajo una gran presión de tiempo, y que debería tomarse como una historia curiosa sobre lo particular que era reproducir el bug en producción.
Fue un error tonto, pero los seres humanos, ya sea individual o colectivamente, cometemos errores tontos.
https://0912i390129ionkjan.bearblog.dev/how-a-single-chatgpt...
https://webcache.googleusercontent.com/search?q=cache%3Ahttp...
El error se veía de inmediato. Respeto al equipo, pero esto no tiene mucho que ver con ChatGPT, sino más bien con que usaron un modelo de programación con el que el equipo no estaba lo suficientemente familiarizado.
Incluso si hubiera pasado una revisión de código, probablemente se habría detectado con herramientas de monitoreo que se configuran en 5 minutos.
Un título como “Cometimos un error de programación al usar un LLM y nos costó 10 mil dólares por no hacer aseguramiento de calidad” no generaría en los ejecutivos una reacción del tipo “¿cuál sería nuestra exposición si ChatGPT arruina algo?”. Habrá montones de gerentes medios y altos compartiendo este artículo en LinkedIn.
Un LLM no puede “cometer errores”. No es determinista, no razona, no piensa ni ejecuta lógica. Es un generador muy vistoso de ensaladas de palabras que usa probabilidades estadísticas, y como no hay garantía de que lo que genera sea correcto o preciso, por definición tampoco corresponde llamarlo error.
Edición: después de que el artículo fuera fuertemente votado en contra por razones obvias, su posición subió de repente, lo que parece indicar que un moderador lo impulsó: https://hnrankings.info/40627558/
Es gracioso que los moderadores hayan impulsado un artículo tan clickbait que, según las reglas, debería haber tenido el título cambiado. Además, que el autor parezca pertenecer a una empresa de Y Combinator seguramente es una completa coincidencia: https://news.ycombinator.com/item?id=40629998
Dijo que para generar el UUID de la clave primaria no había que usar
str(uuid.uuid4()), sino pasar directamente el callableuuid.uuid4, y que SQLAlchemy llamaría a la función al crear el valor. Para el valor por defecto de la fecha, dijo queserver_default=text("(now())")podría no comportarse como se esperaba y recomendó usarfunc.now(); también indicó revisar las importaciones deuuidytextde SQLAlchemy, y considerarDateTime(timezone=True)para manejar zonas horarias.Después propuso como código corregido
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()), unique=True, nullable=False), y ahí agregarlambda:solucionó el problema.uuid.uuid4()como una definición de esquema en algo como Prisma. Por eso este bug en sí no me sorprende, y yo podría haber cometido el mismo error.Aun así, con un solo
kubectl logsse habría detectado de inmediato. Además, ¿pasar de Next.js y Prisma a Python? ¿Por qué?El error en sí se entiende. Incluso escribiendo el código sin ChatGPT, parece algo que se podría pasar por alto con relativa facilidad.
Pero no entiendo por qué no lo detectaron después del primer fallo. ¿Esta empresa no tenía logging? El hecho de que el backend intentara reutilizar un UUID tendría que haber sido evidente al ver el error.
Pasar por error una cadena a una función que espera un objeto callable en lugar de
Stringes común. Si no hubieran usado un ORM, creo que este problema específico se habría evitado, aunque quizá sea mi sesgo personal contra los ORM. Bugs parecidos pueden aparecer perfectamente también en contextos que no sean bases de datos.Las personas que aseguran con tanta confianza que habrían detectado este bug o son ingenieros mucho mejores que yo, o, más probablemente, tienen una percepción algo equivocada de sus propias capacidades.
Dicho eso, que no tuvieran logs o no los miraran sí es realmente difícil de entender. Si era ECS, me parece que la excepción de Duplicate Key se habría propagado a CloudWatch sin configuración adicional; me pregunto si no ocurrió, o si sí ocurrió pero nadie revisó durante la noche qué excepción se había producido.
En una situación así, es útil preguntar por qué la detección fue tardía y por qué el diagnóstico tomó tanto tiempo.
He visto el mismo error varias veces incluso en código escrito por humanos. Especialmente en React / TypeScript / JavaScript, es común que alguien se olvide de una lambda
El post del blog me dio la impresión de que no explica bien la causa raíz del problema y pasa directo a culpar a ChatGPT. Si trabajas con prisa y metes a main un cambio grande o un commit sin revisión de colegas, pasan estas cosas
El verdadero problema es que, si te apuras, tomas atajos y no haces suficientes pruebas ni revisión de código por pares, aparecen errores. Creo que con solo tener pruebas que intentaran varias opciones de suscripción lo habrían encontrado de inmediato
Si pones a alguien así cerca de código financieramente importante, van a surgir problemas parecidos, y me haría cuestionar el criterio de quien decidió desplegar ese código casi sin pruebas
Me sorprende que no hubiera una regla de lint para este caso
Espero que no haya sido así
La parte de “el proyecto original era full-stack NextJS, pero primero quería migrarlo todo a Python/FastAPI” me abrió los ojos
No sé cómo una startup sin clientes puede justificar una reescritura
Con o sin clientes, no entiendo por qué hacer tan pronto un movimiento básicamente lateral de Node a Python. Si tuvieran cientos de clientes y quisieran cambiar a algo como Go, quizá podría entenderlo un poco más, aunque seguiría siendo cuestionable
Por ejemplo, hay que crear un montón de objetos DTO, pero AutoMapper no funciona con la combinación de versiones y la configuración del proyecto que uso, y Entity Framework junto con la serialización/deserialización JSON generan más dolor del valor que aportan
Claro que se puede resolver de forma incremental. Se puede profundizar en la documentación, mezclar hacks, actualizar paquetes y reescribir configuraciones. Pero, como humano, dan ganas de agarrar un bidón de gasolina metafórico, quemarlo todo y hacer que el segundo sistema sea mejor. Por supuesto, en realidad no necesariamente mejora: solo aparecen otros puntos de dolor, y quizá ni siquiera haga todo lo que hacía el primer sistema, o no lo haga bien
En el trabajo me pasa lo mismo cada vez que veo sistemas legacy o engorrosos. Hace falta un esfuerzo activo y constante para ganarle al cerebro que grita que hay que reescribir. A veces una reescritura o un cambio de arquitectura, como adoptar contenedores, sale bien, pero por lo general termina en meterse en el fuego o en una tarea interminable
Salvo que haya una alta confianza en que mejorará la operación del sistema o la experiencia de desarrollo de otros desarrolladores, es mejor no ceder a ese impulso
ChatGPT, más bien, fue quien permitió que la app generara dinero. Sin ChatGPT no tenían la capacidad de implementarla
La falta de capacidad para programar, depurar, hacer logging y monitorear fue lo que tiró a la basura 10 mil dólares; en esta historia, el efecto neto de ChatGPT es positivo
Todos los mensajes de commit tienen emojis. Hay de todo: monos, bananas, cohetes, fuegos artificiales, etc.
https://grook.ai/share?id=e269e88a7b1a71eff4f176c864b30161&x...
Dicen que originalmente era full-stack NextJS y que, al migrar el backend a Python/FastAPI, estaban traduciendo los modelos de base de datos de Prisma/Typescript a Python/SQLAlchemy. Como esa tarea era tediosa y vieron que ChatGPT la hacía bastante bien, lo usaron para casi toda la migración
Si ChatGPT no hubiera existido desde el principio, probablemente no habrían intentado esa migración previa, así que es difícil decir que el efecto neto fue positivo. El stack existente pudo haber tenido mejor logging de errores, o no; y como era código escrito por ellos mismos, quizá conocían mejor su estructura y lo necesitaban menos
La decisión de “volver a escribir todo el código por segunda vez” antes de activar la monetización también es interesante
Hay una frase que dice: “Primero quiero decir que las prácticas aquí fueron malas y que esto se pudo evitar. Ocurrió en otro momento, bajo una gran presión de tiempo. Por favor, léanlo teniendo eso en cuenta”
Por este tipo de restricciones me dan miedo las suscripciones de software
Una vez cobramos dos veces a un usuario por una condición de carrera. Por eso, cuando veo timeouts o errores relacionados con dinero, me vuelvo tan paranoico que primero asumo que el pago sí se hizo y luego lo vuelvo a verificar
¿Código en TypeScript y Python, frameworks como Next.js, 8 tareas de AWS con 5 instancias cada una, y aun así los ingresos eran de 40 dólares, con apenas unas semanas de desarrollo?
Me pregunto qué rayos estaba pasando. Es peor que hayan arreglado el código diciendo que estaba hecho un desastre por las limitaciones de tiempo, pero en realidad gastaron tiempo en refactorizar entre lenguajes y en armar un sistema distribuido sin ninguna razón.
Es complejidad autoinfligida: hacer malabares al mismo tiempo con funcionalidades y una complejidad técnica absurda. No sé qué estaban pensando.
Corrección: es una empresa de YC del verano de 2023, pero parece que en el verano de 2024 el producto seguía detrás de una lista de espera. Probablemente porque lo están reescribiendo en Rust.
Parece que no ha escrito ni 1000 líneas de Python en total, pero identificó bien el problema.
Python tiene un defecto: no copió correctamente la estrategia de evaluación de Common Lisp. Si en la expresión de valor por defecto de un argumento opcional de una función hay algo como
foo=obj.whatever(), entoncesobj.whatever()se evalúa cuando se procesa la definición de la función, no cuando se llama a la función.Sospecho que lo hicieron deliberadamente por eficiencia. Python tiene otro defecto: no cuenta con una sintaxis de literales reales para objetos comunes como listas.
[1, 2, 3]no es un literal, sino más bien un constructor, y cada vez que se evalúa tiene que crear una lista nueva y llenarla con valores.El diseñador no habría querido que un parámetro como
list=[]creara una nueva lista vacía cada vez que se omite el argumento. En Lisp,'(1 2 3)y'()son literales reales y apuntan al mismo objeto cada vez que se referencian. El programador puede elegir si usar(list 1 2 3)o'(1 2 3)como expresión de valor por defecto.El primero crea un objeto mutable nuevo cada vez, como
[1, 2, 3], mientras que el segundo casi con toda seguridad devuelve el mismo objeto y no se puede modificar de forma confiable y portable. Como los lenguajes populares modernos tienen la mayoría de las funciones de Lisp, parece una broma eso de que no hay nada que perder.foo=obj.whatever(),obj.whatever()se evalúa cuando se procesa la definición de la función y no cuando se llama a la función.No sé qué pasaría si
.whatever()dependiera de un estado interno que cambia después de inicializar el objeto.