Got a call from a client. Total panic. They’d just finished a flash sale for their WooCommerce store, and the star attraction—a coupon for 50% off, limited to the first 100 customers—had been used by nearly 250 people. They were on the hook for thousands in discounts they never planned to give. He was not happy. “Your code cost me money, man.”
They were right to be mad. The problem was a classic PHP race condition, something that seems abstract until it hits your bottom line. It’s a nasty bug that only shows up when your site is under heavy load. Exactly when you can’t afford for things to break.
Understanding the PHP Race Condition Mess
So what happened? In WordPress, when you want to track how many times a coupon is used, you’d typically store that number in post meta. The logic seems simple enough: get the current count, add one, and save the new count. Simple. And when I tested it with a single user, it worked perfectly.
My first pass at the code was the obvious one. Trust me, it’s what most devs would write first.
// The obvious, but WRONG way to do it
$coupon_id = 1234;
$usage_count = get_post_meta( $coupon_id, '_usage_count', true );
$usage_count++;
update_post_meta( $coupon_id, '_usage_count', $usage_count );Here’s the kicker. PHP, by its nature, handles every request as a separate, isolated event. It spins up a new process, runs the script, and then it’s done. As Carl Alexander explains in his deep-dive on the topic, it’s not like a persistent application server. During a flash sale, you don’t have one user. You have 50 users clicking “Apply Coupon” at the exact same second. That means 50 separate PHP processes all starting at once.
They all ask the database for the usage count at the same time. If the count is 99, all 50 processes get the answer “99”. They all add one, arriving at 100. Then, one by one, they all tell the database to update the count to 100. The result? You just gave away 50 coupons, but your database thinks you’ve only given away one more. Total nightmare.
The Right Way: Locking the Counter
The fix isn’t more complex code; it’s smarter code. You have to create a “lock” to ensure only one process can update the count at a time. The other processes have to wait their turn. My go-to for this in WordPress is using a transient. It’s a temporary, server-level flag that lets me create a simple locking system.
// The right way, using a transient lock
$coupon_id = 1234;
$lock_key = 'coupon_usage_lock_' . $coupon_id;
// Check if it's already locked. If so, bail.
if ( get_transient( $lock_key ) ) {
return; // Or maybe throw an error
}
// It's not locked. So, WE lock it for 10 seconds.
set_transient( $lock_key, true, 10 );
// Now we can safely read and write
$usage_count = (int) get_post_meta( $coupon_id, '_usage_count', true );
$usage_count++;
update_post_meta( $coupon_id, '_usage_count', $usage_count );
// IMPORTANT: Release the lock so the next process can go.
delete_transient( $lock_key );So, What’s the Point?
The lesson here isn’t just about coupons. It applies to any resource that gets updated frequently under load—inventory counts, booking slots, you name it. Building a site that works for one user is easy. Building one that doesn’t fall apart under the pressure of a successful sales day is a different game entirely. You have to understand how PHP and WordPress actually work under the hood. The simple approach often breaks at scale.
Look, this stuff gets complicated fast. If you’re tired of debugging someone else’s mess and just want your site to work, drop my team a line. We’ve probably seen it before.
Leave a Reply