Blog

How I handled 15k req/min in a Laravel + Vue campaign

Author
Ignacio Amat Ignacio Amat
Published
Reading Time 4 min
Server room with hardware racks and blue lights, representing high-performance infrastructure

Global brand campaigns do not give advance warning before they explode. You get a launch date, an estimated traffic budget, and a tacit expectation that your stack will not fall over. In this case, the real number exceeded the initial estimate by almost an order of magnitude. This is what we did to keep the application responding.

The context: a 48-hour window

The campaign had a 48-hour activation window with concentrated spikes in the first 30 minutes of each phase. The original stack was Laravel + Vue 2 with a monolithic frontend that loaded the entire bundle upfront. Under normal load it worked. Under 15,000 concurrent requests per minute, the server started returning 502s and the frontend froze on mid-range mobile devices.

The problem was not a single bottleneck. It was a cascade: the heavy frontend generated more API requests, the API had no strategic caching, and payment transactions blocked workers synchronously.

Reducing the initial bundle with Vue 3 and Suspense

The first change was migrating the frontend to Vue 3 with route-based lazy loading. We split the application into chunks by route and used Suspense to handle loading states without blocking the main render.

The initial bundle dropped from 180 KB to 95 KB gzipped. On 3G connections, that translated to more than a second of improvement in perceived load time. More importantly: the browser no longer needed to download admin or analytics components that the average user never touched.

We used defineAsyncComponent for secondary routes and kept the critical shell in the entry point. This allowed the first paint to happen before the rest of the application hydrated.

Smart caching with Redis and dynamic TTL

The second bottleneck was in the backend. Every visit queried campaign configuration, product catalog, and stock status. All of that changed, but not every second.

We implemented a caching layer with Redis using dynamic TTLs:

  • Campaign configuration: 5-minute TTL. It only changed if the marketing team activated a new phase.
  • Product catalog: 2-minute TTL. Stock was updated from queues, not from the user’s request.
  • Real-time results: No caching. These endpoints were explicitly marked as skip_cache in the middleware.

The key was being selective. Caching everything creates inconsistencies. Caching nothing creates unnecessary load. We documented every decision in the code so the next developer understood why one endpoint had 120 seconds and another had zero.

Laravel queues for payments and ticket generation

The Bizum integration was the most fragile point. The payment provider had variable latency and a concurrency limit. If we processed payments synchronously, a traffic spike could saturate workers and block requests from users who just wanted to browse the catalog.

We moved the entire payment flow to Laravel queues with Redis as the driver. The user received immediate confirmation that their request was being processed. Meanwhile, a dedicated worker handled communication with the Bizum API, retries with exponential backoff, and email notifications in case of failure.

Separating payment from the user’s request was probably the architectural decision with the biggest impact on stability. Users did not notice the difference, but the system no longer collapsed when the payment provider took 3 seconds instead of 300 ms to respond.

Blue/green deploy with zero downtime

The final risk was deployment. In a 48-hour campaign, you cannot afford 30 seconds of downtime for a migration or hotfix.

We set up blue/green deploys with Laravel Forge. The new version was deployed to a parallel environment, tests and migrations ran, and the traffic switch was instant. If anything failed, rollback took less than 10 seconds.

During the actual campaign we made two minor deploys. Neither generated a single user-reported incident.

The numbers after the refactor

  • Uptime during the campaign: 99.9%
  • API response time p95: 120 ms at peak load
  • Initial bundle: 180 KB → 95 KB
  • Critical production incidents: 0
  • Stack reuse: 3 subsequent campaigns used the same architecture

The most valuable outcome was not the metrics, but that the product team stopped asking “will it hold?” and started asking “when do we launch the next one?”.

Conclusion

Handling high traffic is not just about adding servers. It is about understanding where the real bottleneck is, whether it is in the frontend, backend, or third-party integration. In this case, the solution was a combination of lazy loading, selective caching, and asynchronous queues. None of these techniques are exotic; the hard part is knowing when to apply each one.

If your team is preparing a campaign with uncertain traffic and you use Laravel or Vue, you can review my experience with similar spikes or contact me directly through the form.

Related articles

Review my developer profile

If this article matches the kind of product work your team is facing, review my stack or professional availability.

Send the role context

Role, stack, work model and timing are enough for me to confirm fit. I reply within 24 business hours.

0/500
Availability