A store losing customers
Royal Beef sells Highland beef. A premium product, on average 3-4x more expensive than supermarket standard. In this category customers don't forgive a cart that loses items or a payment that bounces back with an error — they simply don't come back.
The store ran on WordPress with WooCommerce, had 46 active plugins and lived on shared hosting. A configuration we've seen a thousand times: lots of plugins, no control over the server layer, one machine shared with a hundred other shops.
The client came to us with three concrete business symptoms — not with platitudes like "we'd like to speed up the site," but with specifics that were costing money.
Pages were looping
Open a product — timeout. Customer refreshes, loads in 12 seconds. Double-digit bounce rate.
Payments failed at random
Redirect from cart to Przelewy24 returned an error. The customer was stuck with "awaiting payment" status.
Order export failed
Every report ended with manual data entry. An operation that should take seconds was taking hours.
What that chaos really cost.
"The site is slow" is abstract. Concrete costs are team hours and lost orders at peak.
Manual refunds after every weekend
Every few days one customer bought a cut that physically wasn't available. Another had already reserved it earlier. Phone call, apology, manual refund through the P24 panel, digging through email. Each case — fifteen minutes of team time plus damaged reputation.
Customers couldn't complete payment
Redirect from cart to Przelewy24 returned an error at random (not always, not just from a specific browser — just sometimes). The customer saw "try again," the panel showed "awaiting payment," the cart stayed there. Nobody bought a second time.
Order export as torture
Weekly accounting report — click "export XLS" — PHP timeout after 30 seconds. Manual entry from the admin panel. At holiday peak, 200 orders/week = half a day for one person instead of 30 seconds of clicking.
A shop "for insiders"
Royal Beef sells beef at 200-400 PLN/kg. Customers come back for several hundred zlotys monthly. Every such customer has friends they tell "don't buy there, something is always broken." The cheapest customer is the returning one — and Royal Beef was losing those fastest.
Black Friday on the horizon
The migration deadline wasn't "ideally by end of year." It was "Black Friday in 8 weeks." In 2023 the store had crashed at peak — the client remembered. In 2024 it couldn't crash.
30 minute audit
The first thing we do on every WooCommerce audit — open "Site Health" in the WP admin. Second — look at server logs from the last 7 days. Third — inspect autoload options in the database. These three moves immediately showed that this wasn't about "another optimisation plugin."
Three of these numbers explained everything the client reported:
- 40 MB of PHP memory — even though the host declared 512M in php.ini. The limit was hardcoded in
wp-config.phpby a previous developer. Any heavier query (export, report, composite product) ate the free memory and came back as a 500. - No object cache — every product page ran 200+ MySQL queries on every visit. With 46 plugins, that meant hundreds of milliseconds of database time per click.
- No native stock locking — out-of-the-box WooCommerce lets two customers add the same cut to their cart simultaneously. That's not a bug, it's a feature. For a store with unique products, it's a disaster.
How the store lives now. After 6 weeks.
It was not about being "faster" - it was about the client team not having to call their customers to apologize.
Before
- At peak — fifteen minutes weekly on manual refunds
- Failed P24 redirects — several weekly
- Report export — 40 minutes of manual work
- Page loads in 12s at peak hours
- Stress before Black Friday — "are we going to crash again?"
- No visibility into what was happening under the hood
After 6 weeks
- Zero double-sold cuts since deploy — 6 months ago
- Zero failed P24 redirects (the shop got its own queue)
- Report export — 2 seconds, one click
- Site responds in 0.148 s, loads in 2.1 s
- Black Friday 2024: zero downtime, +38% revenue YoY
- Dashboard with store metrics and real-time alerts
The most important change isn't measurable in seconds or gigabytes of memory. It's the change in how many hours per week the Royal Beef team spends on the store instead of on customers, meat and the farm.
Here we describe how. There you see what it looks like.
A case study is the view from the kitchen — technical decisions, trade-offs, code that carries atomic stock locking. The portfolio entry is the same project from the customer's side: what the Royal Beef shop looks like, the colours, the product photography, the cart that finally doesn't lose items.
Royal Beef — a premium Highland beef shop that finally runs the way it should.
See what the shop looks like from the customer's perspective — the meat palette, product cards, cart, checkout. The same delivery, but from the side of the people we built it all for — the customer ordering tenderloin for Sunday who doesn't want to try twice.
Numbers the client sees. And what they mean for business.
All measurements from production - PageSpeed Insights with CrUX field data (28-day rolling), wp-cli for PHP memory, server logs for protocols. No lab tests.
The site responds before the customer notices they clicked. Fewer people bounce on the first product page.
Exports, reports, composite products finally complete instead of returning errors.
Google ranks the store higher. Customers load faster. Better position in search.
| Metric | Before | After | What it means for business |
|---|---|---|---|
| Server response time (TTFB) | 1.8 s | 0.148 s -91% | The customer doesn't have time to get bored before the page starts loading. Lower bounce rate, more visible products. |
| Memory available to the store | 40 MB | 512 MB 12.8x | Exports, reports, composite products finally complete. Zero "error 500" during normal operation. |
| Largest Contentful Paint | no data | 2.1 s good | The customer sees the product and "add to cart" button in 2 seconds. That's the threshold where Google boosts SEO ranking. |
| Cumulative Layout Shift | no data | 0 perfect | The page doesn't jump under the customer's cursor. Fewer accidental clicks on ads instead of products. |
| Double-sold cuts | regular | 0 solved | Zero manual refunds, zero apology phone calls, zero damaged premium-brand reputation. |
| Failed P24 payments | regular | 0 solved | The customer completes payment on the first try. Fewer "awaiting payment" carts to clean up manually. |
| Order export to XLS | 40 min manual | 2 s solved | The accounting report takes seconds. The team works on the shop, not on retyping orders. |
| Server stability at peak | variable | CPU 99.5% idle stable | Black Friday 2024: zero downtime, +38% revenue year-over-year. The shop survived three peaks without intervention. |
| Free VPS memory | 1404 MB | 2355 MB +951 MB | The store has headroom for promotions, more plugins, integrations. Without overpaying for a bigger server. |
| Connection protocol | HTTP/1.1 | HTTP/2 + QUIC verified | The page loads faster especially on mobile (where the network is unstable). Apple, Google, Cloudflare all use this and have for years. |
| Database hygiene | messy | 447 opt. / 66 KB clean | Database entries are counted and don't grow indefinitely. We're building on a clean foundation. |
The first time, at sales peak, we didn't have to manually refund customers for a double-sold cut. We didn't know it could just be like this.
Do you recognize your store in this story?
Most Polish WooCommerce stores with 500K-5M PLN annual turnover have a similar setup. If any of these questions sound familiar - you probably have the same conversation ahead.
It's almost never P24's fault. In 9 out of 10 cases — it's a race condition in the WooCommerce layer: two customers trying to pay for the same cut, one gets an error. Solution: atomic stock locking at the database level.
Almost always it's PHP hitting the memory limit (typically 40-128 MB) and returning a 500. Check "Site Health" — if you see less than 256 MB, the host has hardcoded the limit. It's fixed with a single line in wp-config.php — but someone has to know where to look.
Check PageSpeed Insights with the CrUX field data (not "lab"). If LCP > 2.5s, you're losing positions in Google rankings and customers on the first viewport. Typical diagnosis: no object cache, suboptimal server, too many unnecessary plugins.
Shared hosting has one weakness: you share CPU with a hundred shops next door. At their peak — you feel it. Migration to a dedicated VPS with OpenLiteSpeed gives stable response under pressure — and costs on average 600-1200 PLN/month plus a one-off setup.
That's the moment an LTS (long-term support) contract pays for itself in the first quarter. Patches within 48h, uptime monitoring, quarterly review, dedicated contact — instead of "maybe we should reach out to that developer from three years ago."
For developers: stack, decisions, code.
12 elements of the production stack, 8 architectural decisions with rationale, 3 code blocks from the rb-cart-reservation plugin, request flow diagram.
Engineering appendix Tech stack, architectural decisions and code Expand to see production stack, architectural decisions, plugin code, request flow diagram.
Production stack
12 elements that go into production
Architecture diagram
Cloudflare → OpenLiteSpeed → PHP → Redis / MySQL
Request flow from customer
Architectural decisions
Three decisions that carry the whole project
Migration to a dedicated Ubuntu VPS
Shared hosting cost 80 PLN/month but ate 15-20% of revenue on peak timeouts. The VPS costs 750 PLN/month and gives full control over memory, opcache, my.cnf. Return on investment — one Black Friday without crashes.
Shared hosting doesn't let you tune php.ini, compile extensions or configure my.cnf. Without control over the PHP/MySQL layer, every optimisation is a gamble of "it works if the host doesn't change something behind the scenes."
OpenLiteSpeed instead of Apache
LiteSpeed Cache is a plugin that understands WooCommerce — it knows not to cache cart and checkout, but it does cache product pages. The result: response in 12ms instead of 800ms, with zero changes to the shop's code.
LiteSpeed Cache works directly with the server through a native module, with no .htaccess layer and no mod_php. WooCommerce-aware out of the box, QUIC out of the box, dedicated ESI for dynamic fragments.
Custom plugin rb-cart-reservation
Out-of-the-box WooCommerce lets two customers buy the same cut. For a supermarket with 200 in stock that's not a problem. For Royal Beef with packs from a specific carcass — every such collision means an apology phone call. The plugin locks stock in the database the moment it's added to cart, for 15 minutes.
The key: atomic SELECT ... FOR UPDATE inside a transaction — locks the stock row until commit, so the second process waits. Without it, WooCommerce loses cuts under concurrent traffic, regardless of how many cache plugins you have. Implementation: dedicated rb_stock_reservation table with TTL and CRON cleanup.
Other decisions - briefly
- 04
Redis object cache via socket, not TCP
TCP loopback adds ~80μs overhead per request, plus context switches. Unix socket goes straight through a file descriptor. With 200+ object cache hits per page request, that's a ~16ms difference.
- 05
HPOS + classic postmeta compatibility
The plugin detects on its own whether WooCommerce has High-Performance Order Storage enabled and picks the right table. Migration either way without rewriting code.
- 06
MySQL 8 without deprecated query_cache_type
MySQL 8 removed
query_cache_type— if you leave a 5.7-era entry inmy.cnf, the server boots but throws warnings on every query. A classic creeping bug after migrations. - 07
Cloudflare as CDN + WAF
Static assets from worker edge, WAF blocks bots hammering
/xmlrpc.phpand/wp-login.php. The origin never sees 73% of traffic that never reaches it. - 08
HTTP/2 + QUIC (h3) verified via alt-svc
QUIC needs to be verified with the
alt-svcheader in the response — without it, the client doesn't know the alternative endpoint and won't switch to UDP. The difference between "enabled in config" and "actually in use."
Code
Three blocks carrying the whole plugin
PHP 1. Atomic stock reservation — rb_reserve_stock() class-stock.php
function rb_reserve_stock($product_id, $qty, $session_id) {
global $wpdb;
$table = rb_stock_table(); // HPOS vs postmeta
$ttl = 900; // 15 min hold
$wpdb->query('START TRANSACTION');
// row-level lock — the second process waits
$row = $wpdb->get_row( $wpdb->prepare(
"SELECT stock, reserved FROM {$table}
WHERE product_id = %d FOR UPDATE",
$product_id
));
$available = $row->stock - $row->reserved;
if ($available < $qty) {
$wpdb->query('ROLLBACK');
return new WP_Error('rb_no_stock', __('Out of stock', 'rb'));
}
$wpdb->query( $wpdb->prepare(
"INSERT INTO rb_stock_reservation
(product_id, qty, session_id, expires_at)
VALUES (%d, %d, %s, NOW() + INTERVAL %d SECOND)",
$product_id, $qty, $session_id, $ttl
));
$wpdb->query('COMMIT');
return true;
}PHP 2. HPOS detection — the code picks the table itself helpers.php
function rb_stock_table(): string {
global $wpdb;
// HPOS enabled? Check the WooCommerce feature flag
if (class_exists( '\\Automattic\\WooCommerce\\Utilities\\OrderUtil' )
&& \\Automattic\\WooCommerce\\Utilities\\OrderUtil::custom_orders_table_usage_is_enabled()) {
return "{$wpdb->prefix}wc_orders";
}
// fallback: classic postmeta
return "{$wpdb->prefix}postmeta";
}PHP 3. CRON cleanup with LIMIT 200 (batch) cron.php
add_action('rb_cleanup_reservations', function () {
global $wpdb;
$wpdb->query(
"DELETE FROM rb_stock_reservation
WHERE expires_at < NOW()
LIMIT 200" // batch — don't block the request
);
});
// schedule every minute
if (! wp_next_scheduled('rb_cleanup_reservations')) {
wp_schedule_event( time(), 'rb_every_minute', 'rb_cleanup_reservations' );
}Roadmap
What's next
Frequently asked questions.
How much did all this cost?
The Royal Beef project ran over 6 weeks: audit + plan (1 week), server migration (2 weeks), rb-cart-reservation custom plugin (2 weeks), optimisation and load testing (1 week). Plus LTS care — monthly contract.
Specific numbers we discuss in a conversation — they depend on scale, the stack, how much code has to be written from scratch, how much can be recovered. Every project starts with a free conversation where we tell you what we'd do and what price we're offering. No commitments.
Why didn't we stay on shared hosting?
Shared hosting gives you a fixed, deterministic cost (50-150 PLN/month) and zero ops. But you pay for it in three currencies: you don't control PHP memory or database configuration, plus you share CPU with a hundred shops next door — so you never know whether "it's slow today" is the code's fault or the shop next door's.
For a store doing 500K-5M PLN annual revenue, the difference between shared and a VPS with care (~600-1200 PLN/month) pays for itself after the first Black Friday where you don't lose conversions to timeouts. Royal Beef did the math themselves — one weekend of crashes cost more than a year's hosting difference.
What exactly does "race condition in the cart" mean?
WooCommerce only reserves stock when the order is completed. Between "click add to cart" and "pay now," the stock is technically free — which is fine until you have two customers in the same time window clicking the same cut.
In premium e-commerce with unique products (Royal Beef sells specific packs from a specific carcass) every such collision means a manual refund, a phone call, an apology and lost reputation. The solution: a dedicated plugin that locks stock in the database immediately on add-to-cart, for 15 minutes.
Will I get these numbers too for my store after migration?
Short answer: it depends on the starting point. Royal Beef had an exceptionally bad starting point (40 MB of memory, no object cache, HTTP/1.1) and an exceptionally good ceiling (premium e-commerce, low product count). 12x on response time is the number for that gap.
For a store already on a decent VPS with LiteSpeed or on managed WordPress hosting, a realistic improvement is typically 2-4x on response time and entering the "good" threshold in Google PageSpeed. For comparison with concrete numbers from your store — let's start with a conversation about what's happening in your "Site Health" panel.
Next step
Is your store also losing orders at random?
We'll check it in 30 minutes. No slides, no commitments, no "maybe a revolution." If your store has symptoms similar to Royal Beef — we'll tell you straight away what exactly, in what order, for how much.
- "Site Health" report with concrete numbers from your panel — not generic recommendations
- Core Web Vitals analysis from CrUX — Google data from your traffic, not lab tests
- 3 priority actions to implement in 30 days with concrete business impact
- Upfront cost estimate, without "we'll quote later" and without a commitment to work together
®