Por qué mi App de Lovable se cae con 50 usuarios

Panel de monitorización mostrando picos de latencia y errores durante un colapso

Nos llegan cinco o seis mensajes al mes que dicen casi lo mismo: «funcionaba perfecto en la demo, lo compartimos en LinkedIn, entraron unas cuantas decenas de personas a la vez y se murió». Lo curioso es que casi nunca es culpa del prompt.

Cuando «mi app de Lovable se cae», el patrón que vemos en nuestras revisiones es más aburrido y más reparable: la plataforma generó código que funciona con datos de juguete y tráfico de una sola persona. Nada más. Con 30 filas en una tabla, una lectura sin índice tarda 4 milisegundos. Con 30.000, tarda 900. Multiplica eso por medio centenar de personas pulsando al mismo tiempo y ya tienes el incendio.

Vamos a diagnosticar esto como se diagnostica una avería: primero los síntomas, después las causas reales, luego cómo distinguir cuál es la tuya. Y al final, el orden de intervención. Porque el orden importa más que la técnica.

Los síntomas que aparecen justo antes del colapso

Un producto generado con IA colapsa con 50 personas conectadas por dos causas dominantes: el agotamiento del grupo de conexiones a Postgres, limitado a unas 60 simultáneas en instancias pequeñas, y las lecturas sin índice, que pasan de 4 a 900 milisegundos cuando la tabla crece. Todo lo demás son síntomas.

Y esos síntomas llegan con antelación. Ningún producto se cae de repente: da avisos durante minutos, a veces días. El problema es que nadie los está mirando porque el panel de métricas está en una pestaña que cerraste hace tres semanas.

Peticiones que tardan 8 segundos y luego devuelven error

Este es el síntoma más honesto de todos. Una petición que falla al instante suele ser un bug de código. Una que agoniza durante seis, siete, ocho segundos y luego devuelve un 500 o un timeout está esperando algo: un turno libre, un bloqueo de tabla, la respuesta de un servicio externo.

¿Qué significa esa espera? Que el recurso escaso no es CPU, es turno. Hay una cola. Y las colas no se arreglan optimizando la operación, se arreglan reduciendo cuántas veces la pides.

La app funciona para ti y falla para el resto

Aquí caímos nosotros en un proyecto de febrero, y lo admitimos sin adornos: pasamos dos días buscando un fallo de permisos porque el fundador nos juraba que a él le iba todo fino. Le iba fino porque su navegador tenía la sesión caliente, la caché llena y las tablas con sus propias tres filas de prueba.

Un usuario nuevo, con la tabla ya poblada por los demás y sin nada en caché, ejecutaba una ruta completamente distinta. Misma URL, escenario opuesto.

Todo se cae de golpe y a los diez minutos vuelve solo

La recuperación espontánea es la firma inconfundible del agotamiento del grupo de conexiones o de una cuota por minuto. Cuando algo revive sin que nadie despliegue nada, es porque un contador se reseteó o los enlaces colgados expiraron.

Los fallos de código no se curan solos. Los límites de recursos, sí. Esa distinción te ahorra medio día de búsqueda en la dirección equivocada.

Tabla de síntoma, causa probable y comprobación

Síntoma observado Causa más probable Cómo comprobarlo
Espera de 6-8 s y luego error 500 Grupo de conexiones agotado Panel del proveedor: conexiones activas frente al límite
Se cae y revive solo en 10 minutos Cuota por minuto de funciones Log de invocaciones y contador de cuota del plan
Va bien para ti, mal para los nuevos Caché de sesión y tablas de prueba vacías Reproducir en ventana privada con datos reales
Degradación progresiva semana a semana Listados sin paginación Medir peso de respuesta del endpoint del listado
Una pantalla concreta arrastra a toda la app Patrón N+1 o falta de índice Vista de consultas lentas ordenada por tiempo total
El colapso escala con las pestañas abiertas Suscripciones en tiempo real Contar eventos recibidos por cliente y minuto

Qué está fallando de verdad en la capa de datos

En las auditorías que hemos hecho sobre productos generados con estas herramientas, en torno a siete de cada diez incidentes de saturación nacen en la base de datos. No en el servidor, no en el navegador. En Postgres.

El límite de conexiones simultáneas a Postgres y por qué se agota tan pronto

Las instancias pequeñas de Supabase y proveedores similares suelen permitir en torno a 60 enlaces directos. Suena a mucho. No lo es.

Cada pestaña abierta con una suscripción activa consume uno. Cada función serverless que arranca abre el suyo y a veces no lo cierra. Si tu producto lanza tres peticiones paralelas al cargar el panel principal, medio centenar de personas entrando a la vez reclaman 150 plazas donde hay 60. El resto espera, y a los pocos segundos deja de esperar.

Total, que el número de usuarios concurrentes no es el problema. El problema es cuánto abre cada uno de ellos.

Consultas sin índices: rápidas con 20 filas, letales con 20.000

Postgres no necesita índice para leer 20 filas. Las escanea todas y termina antes de que tú sueltes el ratón. Con 20.000 y un filtro por user_id sin indexar, ese escaneo secuencial pasa de microsegundos a cientos de milisegundos, y bloquea el turno de los demás.

Revisa esto: ¿cuántos índices creó la herramienta más allá de las claves primarias? La respuesta honesta, casi siempre, es ninguno.

Suscripciones en tiempo real que multiplican la carga por cada pestaña abierta

El realtime es la trampa más elegante de todas, porque en la demo queda espectacular. Cada cliente suscrito a una tabla recibe un evento por cada cambio de esa tabla, filtrado o no. Diez usuarios escribiendo generan diez eventos que van a los cincuenta conectados. Quinientos mensajes por cada ronda de escrituras.

Y como cada mensaje suele disparar una recarga de datos en el cliente… ya sabes por dónde va esto.

Los cuellos de botella que la IA genera por defecto

Hay patrones que aparecen con una regularidad casi cómica en el código generado. No porque el modelo sea malo, sino porque optimiza para que funcione, no para que aguante.

Llamadas encadenadas dentro de bucles y el patrón N+1

Traes una lista de 40 pedidos. Luego, dentro del bucle que los pinta, pides el cliente de cada pedido. Cuarenta y una llamadas donde bastaba una con un join. Ese es el N+1, y lo encontramos en la mayoría de proyectos que revisamos.

Con un usuario son 41 y nadie lo nota. Con cuarenta personas viendo esa misma pantalla, son 1.640 en paralelo contra 60 plazas disponibles. La aritmética es implacable.

Listados que descargan la tabla completa sin paginar

Un select sin limit es una bomba de relojería con temporizador de crecimiento. Funciona seis semanas. A la séptima, cuando la tabla llega a 15.000 registros, cada carga de pantalla mueve varios megabytes por la red y obliga al navegador a renderizar miles de nodos.

Si aplicaras paginación de 25 elementos mañana mismo, en la mayoría de los casos verías caer la latencia media a la mitad sin tocar nada más. Es la intervención con mejor relación esfuerzo-resultado que existe.

Efectos sin limpieza que disparan la misma petición varias veces

Un useEffect sin array de dependencias correcto, o sin función de limpieza, ejecuta su llamada cada vez que el componente se vuelve a renderizar. Hemos visto un panel que repetía la misma nueve veces por carga.

Nueve peticiones idénticas, nueve plazas ocupadas, nueve escaneos sin índice. Por usuario.

Arranques en frío y cuotas de las funciones serverless

Las funciones edge que quedan inactivas necesitan entre 200 y 800 milisegundos para despertar. Cuando llega un pico repentino, decenas de instancias arrancan a la vez, cada una reclamando su plaza en la base de datos, y el arranque en frío se convierte en una estampida.

Añade a eso las cuotas por minuto del plan gratuito o inicial, que en muchos proveedores se sitúan en pocos cientos de invocaciones, y tienes el mecanismo exacto de la caída que se cura sola a los diez minutos.

Consultas SQL y esquema de base de datos revisados durante una auditoría de rendimiento

Cómo saber cuál de estas causas es tu causa

Aquí está el error que más dinero cuesta: empezar a parchear por intuición. Vemos gente reescribiendo el frontend entero cuando el cuello estaba en una tabla sin índice.

Leer los logs y las métricas antes de tocar una línea

Mira, lo que pasa es que la información ya está ahí. El panel de tu proveedor de base de datos tiene una vista de consultas lentas ordenadas por tiempo total acumulado. Ábrela. Las tres primeras filas explican, en nuestra experiencia, más del 80% de la degradación.

Nuestra hipótesis inicial en un proyecto de octubre era que el realtime estaba ahogando el sistema. Montamos toda la argumentación. Después de revisar el log, resultó que el 74% del tiempo de base de datos se lo comía un contador de la cabecera que se ejecutaba en cada render. Cambiamos el diagnóstico y arreglamos en cuarenta minutos lo que iba a ser una semana de refactor.

Reproducir la caída con una prueba de carga de 50 usuarios

No hace falta nada sofisticado. Con k6 o Artillery puedes simular 50 usuarios concurrentes en un script de treinta líneas y una tarde de trabajo.

  • Rampa de 0 a 50 en 60 segundos
  • Mantener 3 minutos
  • Registrar p95 de latencia y tasa de error
  • Repetir con la tabla poblada, no vacía

Ese último punto es el que casi todo el mundo se salta. Una prueba de carga contra tablas vacías te dirá que todo va perfecto, y te mentirá.

Aislar cliente, backend y base de datos por separado

Ataca cada capa por su cuenta. Llama al endpoint directamente con curl y mide: si tarda 40 milisegundos, el backend está sano y el problema vive en el navegador. Ejecuta la sentencia cruda contra Postgres: si ahí tarda 900 milisegundos, ya tienes el culpable localizado.

Este ejercicio, que lleva menos de una hora, es exactamente lo que hacemos al abrir cualquier revisión de rendimiento. Si quieres el mapa completo de por dónde vamos, la metodología que seguimos en MVP to SaaS para escalar un MVP hecho con IA parte siempre de este aislamiento por capas antes de proponer un solo cambio de código en producción.

El orden correcto para arreglarlo sin rehacer la app

Existe una secuencia. Saltársela es la razón por la que muchos equipos acaban reescribiendo el producto desde cero cuando no hacía falta.

Primeras dos horas: índices, paginación y consultas duplicadas

Empieza por lo barato y brutal. Crea índices en todas las columnas que aparezcan en cláusulas where, join y order by de tus tres sentencias más lentas. Un create index tarda segundos en tablas de este tamaño.

Después, mete limit y paginación en cada listado. Sin excepciones, ni en las pantallas que «solo tienen pocos registros» (esas son las que crecen).

Ya con eso hecho, revisa los efectos del cliente y elimina las peticiones repetidas. En un proyecto de marzo, estas tres intervenciones bajaron el p95 de 6,2 segundos a 480 milisegundos en una sola sesión de trabajo. (Sí, luego el fundador nos preguntó por qué había pagado por una revisión de dos semanas. Buena pregunta, respondida en la fase siguiente).

Segunda fase: agrupación de conexiones y caché de lecturas

Con lo urgente resuelto, toca el tejido conectivo. Cambia el acceso directo a Postgres por el pooler en modo transacción: pasas de 60 plazas a varios miles de clientes simultáneos sobre el mismo número de enlaces reales. Es el cambio de una línea en la cadena de conexión con mayor impacto que conocemos.

Luego, caché. Los datos que no cambian cada segundo (catálogos, configuraciones, contadores agregados) no tienen por qué recalcularse en cada render. Un TTL de 30 o 60 segundos elimina un porcentaje enorme de la carga de lectura.

¿Funciona esto siempre? Jamás. Si tu producto es intrínsecamente de escritura intensiva, la caché apenas te dará alivio y tendrás que ir directo a la siguiente sección.

Cuándo aceptar que el problema es de arquitectura y no de prompt

Llega un punto en el que pedirle a Lovable que «optimice el rendimiento» genera código distinto pero igual de frágil. Nosotros creíamos que con prompts suficientemente detallados se podía llegar bastante lejos; después de una docena de proyectos, cambiamos de opinión: el techo no está en la calidad de la instrucción, está en que el modelo no tiene visión del sistema completo ni de cómo se comporta bajo concurrencia.

Las señales de que cruzaste ese umbral son concretas. Necesitas colas de trabajos. Necesitas transacciones con lógica de negocio real. Necesitas control sobre reintentos e idempotencia. Nada de eso se resuelve reformulando una frase.

Diagrama de arquitectura en pizarra separando capas de cliente API y datos

El umbral en el que conviene sacar el backend fuera

No hay que huir del entorno gestionado a las primeras molestias. Es rápido, es cómodo y para la mayoría de productos en validación es la decisión correcta durante bastante tiempo. Pero tiene un techo, y conviene reconocerlo antes de estrellarse contra él con clientes que pagan.

Señales de que ya no escalas dentro del entorno gestionado

Estas son las que nos hacen recomendar la salida:

  1. Necesitas procesos en segundo plano de más de 30 segundos
  2. Tienes lógica de negocio que debe ejecutarse de forma transaccional y auditable
  3. Los límites de invocaciones te obligan a subir de plan cada seis semanas
  4. Requisitos de cumplimiento sobre dónde y cómo se almacenan los datos
  5. Cada cambio de esquema rompe tres pantallas que nadie tocó

Con dos de estas cinco, ya merece la pena hacer números. Con cuatro, la migración es más barata que seguir parcheando.

Qué preparar antes de migrar para no romper nada

Sacar el backend fuera sin preparación es cambiar un problema de rendimiento por un problema de disponibilidad. Antes de mover una sola tabla: documenta el esquema real (no el que crees que tienes), escribe pruebas de los cuatro o cinco flujos que generan ingresos, y monta la nueva capa en paralelo antes de apagar la vieja.

Ese inventario previo es, en la práctica, el 60% del trabajo de migración, y es también lo primero que entregamos en cualquier auditoría técnica de MVP to SaaS que nos encargan sobre un producto generado con IA, precisamente porque sin ese mapa nadie puede estimar con honestidad cuánto cuesta el cambio.

Preguntas frecuentes

¿Por qué se cae mi app de Lovable?

En la mayoría de casos que revisamos, la caída viene del agotamiento del grupo de conexiones a Postgres (unas 60 en instancias pequeñas) combinado con lecturas sin índice y listados sin paginar. Son fallos del proyecto generado, no del servicio.

¿Cuántos usuarios simultáneos soporta una app hecha con Lovable?

No hay un número fijo: depende de cuántas peticiones abre cada sesión. Un producto que lanza tres llamadas paralelas por pantalla se ahoga a las 20 personas; el mismo producto con índices, paginación y pooler aguanta varios cientos sin cambiar de plan.

¿Se pueden usar apps de Lovable en producción?

Sí, siempre que pasen antes por una revisión de capa de datos. El código generado está optimizado para funcionar en la demo, no para aguantar concurrencia, y esa diferencia se cierra en horas, no en meses.

¿Cómo saber si el problema es de Lovable o de mi app?

Comprueba primero si hay incidencias públicas del proveedor. Si no las hay y tu caída se cura sola a los diez minutos, es un límite de recursos de tu proyecto: cuota por minuto o conexiones agotadas. Los fallos de plataforma afectan a todos a la vez.

¿Cómo mejorar el rendimiento de una app creada con Lovable?

En este orden: índices en las columnas de where, join y order by; paginación de 25 elementos en cada listado; eliminación de peticiones duplicadas en el cliente; pooler en modo transacción; y caché de lecturas con TTL de 30 a 60 segundos. En un proyecto real esa secuencia bajó el p95 de 6,2 segundos a 480 milisegundos.

Conclusión

Que un producto colapse con las primeras decenas de personas conectadas no significa que esté mal construido. Significa que fue construido para demostrar una idea, y ahora se le está pidiendo otra cosa.

El diagnóstico casi siempre cabe en tres frases: falta algún índice, sobran peticiones y las plazas de conexión se agotan antes de lo que nadie esperaba. Empieza por los logs, reproduce la caída con una prueba de carga contra datos reales, y ataca en orden. La arquitectura solo se toca cuando lo demás ya está descartado.

Y guarda la energía. La versión que aguanta mil usuarios no se escribe de una vez: se llega a ella arreglando, en el orden correcto, la que aguantaba cincuenta.

Founder & Software Engineer
Ingeniero de Software y Desarrollador Full Stack con más de 10 años de experiencia operativa. Dirige MVPtoSaaS aplicando los mismos estándares de fiabilidad y rendimiento exigidos en sistemas industriales y de alta criticidad. El enfoque de Felipe asegura que cada producto que transita por MVPtoSaaS cuente con código limpio, bases de datos optimizadas para alta concurrencia y despliegues automatizados preparados para el crecimiento sostenido sin deuda técnica.