Ecommerce capacity planning fails in one specific way: the server is sized for the average day, and the average day is not what breaks it. A storefront doing 40 requests per second at noon on a Tuesday will do 600–900 when the promo email lands, and the failure is almost never “not enough disk” — it is the PHP worker pool saturating while MySQL sits at 20% CPU, or the reverse after the cache warms differently under checkout-heavy traffic.
The useful model treats traffic as a request mix, not a single RPS number. Catalog browsing is cacheable and cheap; cart, checkout, and account pages bypass most caching layers and cost 8–20× more CPU. A peak where 85% of requests are catalog pages behaves completely differently from the same RPS where 30% are checkout.
Start by splitting the workload in two. Measure requests per second through the web tier and separately the queries per second hitting MySQL during a 10-minute peak sample. On a decently tuned WooCommerce or Magento store you will see roughly 4–12 queries per cached page request and 40–90 per uncached checkout request. That ratio drives everything below, and the dedicated server hardware thresholds in this guide assume it.
The PHP and database split
Putting web and database on the same box is fine up to about 250 RPS with a warmed cache, and a bad idea past it. PHP-FPM is CPU-bound and bursty; MySQL is memory-bound and sensitive to disk latency. When they share eight cores, a checkout burst starves the database of CPU and the reply buffers grow, which turns a 200ms query into a 2s query. Separate them once you exceed 4 PHP-FPM workers per core, or once MySQL’s buffer pool cannot hold the working set.
As a starting allocation on a mid-sized store (150–300 RPS peak), give the web tier 6–8 cores at 3.5GHz+ and the database 8 cores with 32–64GB RAM, on NVMe with p99 write latency under 1ms. Run SHOW ENGINE INNODB STATUS and check the buffer pool hit ratio before adding cores — below 99% on a dedicated box means RAM, not CPU, is the limit.
Cache hit ratio changes the hardware, not the traffic
Full-page cache hit ratio is the single biggest lever on server cost. At 90% hit ratio, 300 RPS is 30 requests touching PHP and MySQL. At 65%, it is 105 — three and a half times the compute for identical traffic. Small things move this number: logged-in cookie handling, cart fragments, personalised banners, and whether the cache varies by session.
Push the hit ratio up before buying hardware. Serve catalog pages from Varnish or a CDN edge with a 5–15 minute TTL, use ESI or AJAX for cart counts, and set a stale-while-revalidate policy so a cache miss under load does not stampede the origin. Getting from 65% to 90% is usually worth more than doubling the core count, and it costs nothing at renewal time.
Write amplification from order tables
Orders are not just inserts. Each checkout touches orders, order_items, order_address, inventory counters, and often a sales-flat table, and every one of those updates writes a full 16KB InnoDB page rather than the few hundred bytes changed. A 4-row checkout can generate 60–150KB of physical writes. At 20 checkouts per second during a flash sale, that is 1.2–3MB/s of sustained random writes — trivial for enterprise NVMe, painful for SATA SSD or a shared SAN.
Reduce it before scaling it: keep innodb_flush_log_at_trx_commit=1 for orders but batch inventory decrements, avoid updating a single hot counter row from every checkout (use a queue), and archive completed orders monthly so the working set stays in the buffer pool. Check Innodb_data_written against the bytes actually inserted — a ratio above 10:1 means write amplification is your real cost.
Black Friday load-test profile
A realistic peak test is not a flat 500 RPS loop. Below is a mix worth reproducing on staging with a warmed cache, based on typical apparel and electronics stores.
| Phase | Duration | RPS | Checkout share | Cache hit |
|---|---|---|---|---|
| Warm | 10 min | 120 | 4% | 92% |
| Ramp | 5 min, +150 RPS/min | 120→800 | 8% | 90% |
| Doorbuster | 15 min | 800 | 25% | 78% |
| Sustained peak | 60 min | 600 | 12% | 86% |
| Decay | 20 min | 600→150 | 5% | 91% |
Reading the results
The pass criteria: p95 response under 800ms across the ramp, no PHP-FPM listen queue above 5, MySQL p99 query under 50ms, and zero 5xx. If the doorbuster phase pushes p95 past 2s but the sustained phase is fine, you are one core-short on the web tier or the cache is collapsing under personalised requests. If MySQL p99 climbs while PHP is idle-waiting, you have a missing index or a lock contention problem, not a hardware problem.
Run the test against a database restored from production, not a seeded sample — row counts and index selectivity change execution plans. And run it twice: the second run reveals what the cache warm-up actually covers.
Headroom rules and the buy decision
Buy to hold sustained peak at 60% CPU and 70% of the buffer pool, so the doorbuster’s extra 30% does not tip you over. That usually means sizing for 1.5× your measured Black Friday peak, not your observed average. Keep a scale-out path: a load balancer in front of two web nodes is cheaper than a single 32-core machine and gives you maintenance without downtime.
Order the disk for write latency, the RAM for the working set, and the cores for checkout concurrency. Decide those three and the spec writes itself; the dedicated plans split across web and database give you the isolation needed for peak season.
Leave a Reply
You must be logged in to post a comment.