Free code snippet vs plugin for WooCommerce purchase limits
“Why pay for a plugin when I can just paste a snippet?” is a completely fair question, and for some stores the snippet is genuinely the right answer. This post gives you the snippet to copy, an honest accounting of what it can and can’t do, and a clear rule for when the snippet quietly becomes the more expensive option.
Start with the free path — really
If you’re protecting a single limited product for a single drop, you may not need to buy anything. WooCommerce gives you two free levers out of the box:
- Sold individually (Product › Inventory) — caps the cart at quantity 1. Stops someone buying 5 in one order. Does not stop a second order.
- Min/Max Quantities (official extension) — finer per-cart control. Still per-cart, still resets each order.
If “one per cart” is genuinely all you need, stop here — you’re done for free. The rest of this article is for when you need one per customer across every order, which neither of those does. (We unpack exactly why in the hidden flaw in WooCommerce’s max quantity setting.)
The working snippet
Here’s a real, functional snippet that enforces a lifetime per-customer limit on one product, covering both logged-in users (by ID) and guests (by billing email at checkout). Paste it into a code-snippets plugin or your child theme’s functions.php:
// 1) Block logged-in repeat buyers at add-to-cart.
add_filter( 'woocommerce_add_to_cart_validation', function( $passed, $product_id, $qty ) {
$target = 12345; $limit = 1;
if ( (int) $product_id !== $target || ! is_user_logged_in() ) {
return $passed;
}
$bought = droplock_count_purchased( $target, get_current_user_id(), '' );
if ( $bought + $qty > $limit ) {
wc_add_notice( 'You have already purchased this limited product.', 'error' );
return false;
}
return $passed;
}, 10, 3 );
// 2) Block guests (and double-check everyone) at checkout, by billing email.
add_action( 'woocommerce_after_checkout_validation', function( $data, $errors ) {
$target = 12345; $limit = 1;
$email = sanitize_email( $data['billing_email'] ?? '' );
$uid = get_current_user_id();
$bought = droplock_count_purchased( $target, $uid, $email );
$in_cart = 0;
foreach ( WC()->cart->get_cart() as $ci ) {
if ( (int) $ci['product_id'] === $target ) { $in_cart += (int) $ci['quantity']; }
}
if ( $bought + $in_cart > $limit ) {
$errors->add( 'limit', 'You have already reached the purchase limit for this product.' );
}
}, 10, 2 );
// Helper: count prior purchases by user id and/or email, de-duplicated by order.
function droplock_count_purchased( $product_id, $user_id, $email ) {
$ids = array();
$args = array( 'status' => array('completed','processing','on-hold'), 'limit' => -1, 'return' => 'ids' );
if ( $user_id ) { $ids = array_merge( $ids, wc_get_orders( $args + array('customer_id' => $user_id) ) ); }
if ( $email ) { $ids = array_merge( $ids, wc_get_orders( $args + array('billing_email' => $email) ) ); }
$ids = array_unique( $ids );
$total = 0;
foreach ( $ids as $oid ) {
$order = wc_get_order( $oid );
if ( ! $order ) { continue; }
foreach ( $order->get_items() as $item ) {
if ( (int) $item->get_product_id() === (int) $product_id ) {
$total += (int) $item->get_quantity();
}
}
}
return $total;
}
This is close to what a basic plugin does internally. It’s honest, it uses the HPOS-safe wc_get_orders() API, and it covers both customer types. If your needs match its assumptions, ship it.
What the snippet is genuinely great at
- Zero cost, zero dependencies. No plugin to vet, update, or trust.
- Total control. It’s your code; tweak the logic however you like.
- One product, one launch. If that’s the whole job, this is the leanest possible solution.
What it can’t do (without becoming a project)
The snippet above is the simple version. Each gap below is fixable — but every fix is more code you write and maintain:
| Need | Snippet status |
|---|---|
| Protect multiple products | Copy/edit per product, or rewrite to read per-product config |
| Variations share one limit | Not handled — needs parent/variation roll-up logic |
| Different limit per product | Hard-coded; needs a settings store |
| Admin / shop-manager bypass | Missing — you’ll block your own manual orders |
| Cart-page quantity changes | Not covered — needs the update-cart hook too |
| Custom message & product badge | Generic error only |
| Configurable counted statuses | Hard-coded array |
| A log of who got blocked | None |
| Performance on big stores | limit => -1 every check; needs caching |
| Survives WooCommerce updates | Your responsibility, forever |
The real cost of a snippet isn’t the code
The snippet is free to paste. The cost shows up later: it’s the afternoon you spend adding variation support, the launch where you discover guests slipped through because you only checked logged-in users, the support ticket from a customer who got a confusing generic error, and the WooCommerce 9.x update that quietly changes a hook signature. None of that is hard individually. It’s death by a thousand small maintenance tasks — and it always lands during a launch, never on a quiet Tuesday.
A simple decision rule
- Use the snippet if: one product, one drop, simple (not variable), you’re comfortable in PHP, and you’ll be watching the launch live.
- Use a plugin if: two or more products, variable products, you want guest + logged-in handled correctly, you need an admin bypass, a branded message, or a log — or you simply don’t want to own WooCommerce code through every core update.
At one product, the snippet is cheaper. By the third product, the plugin almost always is — once you value your time at more than zero.
What a plugin buys you
A focused plugin is the snippet’s entire feature wishlist, already built and maintained: per-product limits, variation roll-up, guest + logged-in matching merged and de-duplicated, cart and checkout validation, admin bypass, custom messaging, a badge, and a blocked-attempt log — configured per product instead of per code edit.
DropLock is that plugin
Everything in the wishlist above, as one toggle per product. The free version is open source on GitHub (GPL) and protects one product; Pro removes the one-product limit and adds custom messaging, the full log, and the roadmap. 14-day refund.
Compare Free vs Pro →