Sizing production for a replatform launch — from real traffic data, not guesswork
The question every replatform has to answer
The day you leave a SaaS platform like Shopify, capacity stops being someone else's problem. Shopify absorbs your Black Friday; your own servers don't — unless you sized them for it. Before our first market launch I had to answer, with real money on the line: how much server do we actually need?
Most teams answer this by feel — buy something big, hope it holds. I answered it with the one dataset every replatforming business already owns: the analytics of the store being replaced.
Step 1: Profile the real traffic
The existing Shopify store had years of traffic history. From Shopify Analytics I worked primarily with two reports: "Online store sessions over time" at hourly granularity and "Total sales over time", profiled across three distinct regimes:
- Weekdays — the baseline: steady, predictable session curves peaking in the evening.
- Weekends — consistently heavier, with different peak hours.
- Sale periods — the regime that actually matters for sizing: traffic multiples of the weekday baseline, compressed into peak hours, with checkout (the most expensive path) over-represented.
From the sale-period peaks I derived a target load envelope: peak-hour sessions and orders converted into requests per second, with headroom on top — because the number that sizes your infrastructure is not your average day; it's your best day, plus margin.
Step 2: Translate traffic into hardware — two different bottlenecks
The frontend and backend of a Laravel + Next.js stack don't scale on the same axis, so they got different server profiles:
- Frontend (Next.js SSR): CPU-bound. Server-side rendering burns cores, not memory. The frontend box was chosen for CPU capacity and ran 8 PM2 fork processes on distinct ports behind an nginx least-connections upstream — real load distribution plus one-at-a-time graceful recycling.
- Backend (Laravel / PHP-FPM): worker-count-bound. Each PHP-FPM worker consumes a roughly fixed slice of RAM, so maximum concurrent requests is a memory equation. The backend boxes were chosen RAM-rich to maximize the FPM worker pool.
Topology at launch: deliberately small but redundant — one frontend and two backend servers behind a Cloudflare load balancer. Not the biggest instances money could buy: the instances the data said we needed, with a scaling path if the data changed. All three sat with a single hosting provider — a concentration whose true cost became clear later (see the failover postmortem).
flowchart TB
A["Shopify Analytics — 'Online store sessions over time' (hourly) + 'Total sales over time'"] --> B["Traffic regimes: weekday baseline · weekend · sale-period peaks"]
B --> C["Target RPS envelope = sale peak + headroom"]
C --> D1["Frontend: CPU-bound Next.js SSR — CPU-optimized box, 8 PM2 forks behind nginx least_conn"]
C --> D2["Backend: RAM-bound PHP-FPM — RAM-rich boxes, maximum worker pool"]
D1 --> E["Launch topology: 1 frontend + 2 backend behind a Cloudflare LB"]
D2 --> E
E --> F["k6 proof: 24,440 requests, ~198 RPS sustained, zero HTTP errors"]
F -. "findings feed back: FPM is the ceiling, DB at 25%, two mis-keyed rate limiters removed" .-> E
The sizing pipeline: analytics → traffic regimes → hardware math per bottleneck → load-test proof, with findings fed back into the topology.
Step 3: Prove it before customers do
Projections are hypotheses. Before launch I wrote k6 load-test shell scripts, ran them against the production stack while watching from the server side, and got the envelope confirmed: 24,440 requests over 2 minutes at ~198 RPS sustained, zero HTTP errors — comfortably above the derived sale-peak target.
The test earned its keep twice over. It identified PHP-FPM as the true scaling ceiling (the database sat at 25% connection capacity — so scaling means more FPM capacity, not a bigger DB). And it exposed two mis-keyed rate limiters — one bucket shared by all anonymous traffic behind Cloudflare's NAT — that had been silently causing a ~6% masked failure rate. They were removed and replaced with a deliberate access policy: the frontend servers whitelisted for unthrottled GraphQL, everything else rate-limited.
The payoff — twice
The launch and the first sale period ran on this topology without a capacity incident: page performance held through peaks the analytics had predicted almost to the hour.
The second payoff came under much worse circumstances. When the hosting provider itself suffered a total outage mid-sale (that story is its own postmortem), I had to stand up an emergency environment at a second cloud provider with a €40K+/hour sale running. Because the capacity envelope was already known, resizing the failover box was arithmetic, not guesswork — I set it to 8 vCPU / 32 GB RAM before it saw a single customer, confident it would hold peak sale traffic. It processed 4,000+ orders that day without strain.
The principle
Capacity planning is data + arithmetic + a load test. The traffic data tells you what to expect, the hardware math tells you what to buy, and the load test tells you whether you were right — before your customers do. "Buy the biggest instance" is not a strategy; it's a way of paying monthly for not knowing your own numbers.