Blog

Cómo manejé 15k req/min en una campaña Laravel + Vue

Autor
Ignacio Amat Ignacio Amat
Publicado
Lectura 5 min
Sala de servidores con racks de hardware y luces azules, representando infraestructura de alto rendimiento

Las campañas de marca globales no avisan cuando van a explotar. Te dan una fecha de lanzamiento, un presupuesto de tráfico estimado y una expectativa tácita de que tu stack no se caiga. En este caso, la cifra real superó la estimación inicial por casi un orden de magnitud. Esto es lo que hicimos para que la aplicación siguiera respondiendo.

El contexto: una ventana de 48 horas

La campaña tenía una ventana de activación de 48 horas con picos concentrados en los primeros 30 minutos de cada fase. El stack original era Laravel + Vue 2 con un frontend monolítico que cargaba todo el bundle al inicio. Bajo carga normal funcionaba. Bajo 15.000 peticiones concurrentes por minuto, el servidor empezaba a devolver 502s y el frontend se congelaba en móviles de gama media.

El problema no era un solo cuello de botella. Era una cascada: el frontend pesado generaba más requests al API, el API no tenía cacheo estratégico y las transacciones de pago bloqueaban workers de forma sincrónica.

Reducir el bundle inicial con Vue 3 y Suspense

El primer cambio fue migrar el frontend a Vue 3 con route-based lazy loading. Dividimos la aplicación en chunks por ruta y usamos Suspense para manejar estados de carga sin bloquear el renderizado principal.

El bundle inicial pasó de 180 KB a 95 KB gzipped. En conexiones 3G, eso se tradujo en una mejora de más de un segundo en el tiempo de carga perceptible. Más importante aún: el navegador ya no necesitaba descargar componentes de administración o análisis que el usuario promedio nunca tocaba.

Usamos defineAsyncComponent para las rutas secundarias y dejamos el shell crítico en el entry point. Esto permitió que el primer paint ocurriera antes de que el resto de la aplicación se hidratara.

Cacheo inteligente con Redis y TTL dinámico

El segundo cuello de botella estaba en el backend. Cada visita consultaba configuración de campaña, catálogo de productos y estado de stock. Todo eso cambiaba, pero no cada segundo.

Implementamos una capa de cacheo con Redis usando TTLs dinámicos:

  • Configuración de campaña: TTL de 5 minutos. Cambiaba solo si el equipo de marketing activaba una fase nueva.
  • Catálogo de productos: TTL de 2 minutos. El stock se actualizaba desde colas, no desde la request del usuario.
  • Resultados en tiempo real: Sin cacheo. Estos endpoints eran explícitamente marcados como skip_cache en el middleware.

La clave fue ser selectivos. Cachear todo genera inconsistencias. No cachear nada genera carga innecesaria. Documentamos cada decisión en el código para que el siguiente developer entendiera por qué un endpoint tenía 120 segundos y otro cero.

Colas Laravel para pagos y generación de tickets

La integración con Bizum era el punto más frágil. El proveedor de pagos tenía latencia variable y un límite de concurrencia. Si procesábamos los pagos de forma sincrónica, un pico de tráfico podía saturar los workers y bloquear requests de usuarios que solo querían ver el catálogo.

Movemos todo el flujo de pagos a colas Laravel con Redis como driver. El usuario recibía una confirmación inmediata de que su solicitud estaba en proceso. Mientras tanto, un worker dedicado manejaba la comunicación con la API de Bizum, reintentos con backoff exponencial y notificaciones por email en caso de fallo.

Separar el pago de la request del usuario fue probablemente la decisión de arquitectura con mayor impacto en la estabilidad. Los usuarios no notaban la diferencia, pero el sistema ya no colapsaba cuando el proveedor de pagos respondía en 3 segundos en lugar de 300 ms.

Deploy blue/green sin downtime

El último riesgo era el deploy. En una campaña de 48 horas, no puedes permitirte 30 segundos de downtime para una migración o un hotfix.

Configuramos deploys blue/green con Laravel Forge. La nueva versión se desplegaba en un entorno paralelo, se ejecutaban los tests y las migraciones, y el switch de tráfico era instantáneo. Si algo fallaba, el rollback tomaba menos de 10 segundos.

Durante la campaña real hicimos dos deploys menores. Ninguno generó una sola incidencia reportada por usuarios.

Los números después del refactor

  • Uptime durante la campaña: 99.9%
  • API response time p95: 120 ms en pico de carga
  • Bundle inicial: 180 KB → 95 KB
  • Incidencias críticas en producción: 0
  • Reutilización del stack: 3 campañas siguientes usaron la misma arquitectura

Lo más valioso no fueron las métricas, sino que el equipo de producto dejó de preguntar “¿aguantará?” y empezó a preguntar “¿cuándo lanzamos la siguiente?”.

Conclusión

Manejar tráfico alto no es solo añadir servidores. Es entender dónde está el cuello de botella real, si está en el frontend, en el backend o en la integración con terceros. En este caso, la solución fue una combinación de lazy loading, cacheo selectivo y colas asíncronas. Ninguna de estas técnicas es exótica; lo difícil es saber cuándo aplicar cada una.

Si tu equipo está preparando una campaña con tráfico incierto y usas Laravel o Vue, puedes revisar mi experiencia con picos similares o contactarme directamente desde el formulario.

Artículos relacionados

Revisa mi perfil como desarrollador

Si este artículo encaja con los retos técnicos de tu equipo, revisa mi stack o mi disponibilidad profesional.

Envíame el contexto del rol

Con rol, stack, modalidad y timing ya puedo decirte si encaja avanzar. Respondo en menos de 24 horas hábiles.

0/500
Disponibilidad