A retailer launches a limited sneaker drop at noon. By 12:03 the product page shows "sold out." By 12:20 it is back in stock, then gone again at 12:21. Orders paid for in that window: a fraction of the units that vanished. The rest sat in carts that nobody intended to pay for, held by sessions that renewed the hold every few minutes.
This is inventory hoarding, and it is a different problem from scalping or card testing. The bot does not want to buy. It wants real shoppers to be unable to. OWASP classifies the behavior as OAT-021, Denial of Inventory, in its Automated Threats to Web Applications handbook (OWASP, 2015).
Most bot guides skip the first engineering question: does add-to-cart reserve stock in your store at all? If it does not, hoarding is mostly a nuisance. If it does, every cart is a lock on revenue. This post walks through where the hold begins, how it expires, where bots exploit renewal, what the logs look like, and which application controls cap the damage before you ever reach for ecommerce bot protection at the edge.
Denial of inventory vs scalping vs card testing: what this post covers
Three bot problems get lumped together under checkout fraud. They need different fixes, so separate them first.
Scalping (OWASP OAT-005) means obtaining limited-availability or preferred goods or services by unfair methods, such as buying scarce items to resell. The bot completes payment. Your inventory count is accurate, you just sold to the wrong party. Card testing (OAT-001, carding) means running stolen card numbers through your payment step to see which ones work. It hits your payment endpoint and your processor fees. Denial of inventory (OAT-021) means holding stock without paying. The bot adds items to carts or starts checkouts and never finishes.
The tell is the conversion shape. Scalping shows high paid conversion from odd buyers. Card testing shows many small payment attempts with high decline rates. Denial of inventory shows a high rate of carts, reservations or checkout starts that expire unpaid, often with a suspiciously regular rhythm.
This post covers only the third. It is also not a general abandoned-cart guide. Real shoppers abandon carts constantly, and a reservation system has to tolerate that. The goal is to cap how much stock any one actor can tie up, and to make sure bots cannot do it at scale for free.
If your concern is the payment side, the post on checkout fraud detection separates bot traffic from payment risk. Come back here when the symptom is stock that disappears and does not convert.
Does add-to-cart reserve stock in your store? Map where reservation begins
Don't assume. Platforms differ, and customizations differ more. There are four common reservation points, and your store may use more than one.
- No reservation until payment. Add-to-cart only records intent. Stock decrements when the order is placed or the payment is captured. Hoarding through carts does nothing to availability, though it can still distort analytics and waste server work.
- Reservation at add-to-cart. The cart line item decrements available quantity or creates a hold record. This is the most exposed design, because the cheapest action (one POST) takes stock.
- Reservation at checkout start. Stock is held when the shopper enters the checkout flow or submits shipping details. Slightly harder to automate, still scriptable.
- Reservation at payment authorization. Stock is held while the payment is pending, typically for minutes. The window is narrow, but a bot can open many pending authorizations.
To map yours, do this today. Pick a product with a stock of three. Add two units to a cart in one browser, then check availability from a second browser with no session. Note when availability changes. Repeat after beginning checkout, and again after entering payment details without submitting.
Then read the code or the platform configuration that owns the decrement. Look for a hold table, a reservation service, a database column such as reserved_quantity, or an inventory API call made from the cart controller. Write down the exact endpoint that creates the hold and who is allowed to call it.
That endpoint is your protected route. Everything later in this post attaches to it.
How holds are created, expired and renewed (and where bots exploit renewal)
A hold has three lifecycle events: creation, expiry and renewal. Bots abuse the third.
Creation needs a quantity, a product or variant identifier, and an owner. The owner is a session ID, a cart token or a customer account. Check how cheaply an owner can be minted. If a fresh anonymous session can create a hold with one unauthenticated request, a script can mint thousands of owners.
Expiry is usually a timer: 10, 15 or 30 minutes from creation. Check where the timer lives. A TTL on a Redis key expires reliably. A scheduled job that sweeps expired holds every hour does not, and stock stays locked until the next sweep. Also check what happens on a failed sweep. A hold with no working expiry is a permanent lock.
Renewal is where designs leak. Common renewal triggers include any cart update, any page view that touches the cart, a checkout step transition, and a payment retry. If viewing the cart page resets the timer, a bot that polls the cart every 9 minutes holds stock indefinitely with trivial traffic. The same applies to quantity edits: change 2 to 3 to 2, and the clock restarts.
Two patterns show up repeatedly in abuse. The first is renewal polling: a low-rate request on a fixed interval just under the TTL. The second is session rotation: when a hold nears its cap, the script creates a new session and re-adds the item, so no single owner looks greedy.
Fix renewal at the design level. Make the TTL absolute from creation, or allow a limited number of renewals (for example, one extension tied to a real checkout step). Renewal should require progress toward payment, not mere presence.
Abandoned shopping vs coordinated reservation abuse: what to look for in logs
You can't cap what you can't see. Start with the hold records or the cart and checkout endpoint logs, and look for shape rather than volume.
Human abandonment is messy. Hold durations vary widely. Many carts include several products. Sessions load images, scripts and other pages. Adds cluster around normal browsing hours. A fraction convert, and the ones that don't simply expire at the TTL or earlier.
Coordinated abuse is tidy. Look for these patterns in the logs:
- Expiry-to-re-add gaps near zero. The same product re-added seconds after a hold expires, from a new session.
- Single-SKU focus. Sessions that touch only the constrained product, with no browsing history.
- Maximum quantity every time. Every add uses the per-order limit.
- Fixed renewal intervals. Cart reads or updates at near-identical spacing.
- Zero paid conversion across a cluster. Group by whatever is stable (account, payment fingerprint, shipping postcode, request headers) and compare cart-to-paid ratios.
- Missing downstream steps. Holds created without the usual preceding page requests or asset loads.
Here's a worked comparison. Take the last constrained drop and compute, for each hold, its lifetime and whether it converted. If 70 percent of held units came from owners with a lifetime paid-conversion of zero, you have either hoarding or a very unusual customer base. Cross-check by owner count: a handful of IP addresses or ASNs behind thousands of owners is a strong signal, though residential proxies can blur it. For that case, the guide on residential proxy scraper detection covers why address-based grouping fails and what to compare instead.
Keep the raw evidence before you block anything. The checklist in scraper incident response applies equally to hoarding incidents.
Application controls that cap stock tied up without purchase
Application controls bound the damage even when detection is imperfect. They should work with zero bot intelligence, because bots will eventually evade detection. Build them in this order.
1. Cap held units per owner and per product. For a limited-stock item, set a per-owner ceiling (often one or two units) and enforce it server-side at hold creation. The cap must apply across the owner's active carts, not per cart.
2. Cap unpaid stock as a share of available inventory. Most stores lack this control. Define a ceiling on total held-but-unpaid units for a constrained SKU, for example 30 percent of remaining stock. When the ceiling is hit, new holds queue or fail gracefully instead of draining the shelf. Pick the percentage from your normal conversion data, and tune it per drop.
3. Shorten and harden the TTL. For scarce items, hold times of 5 to 10 minutes reduce exposure. Make the expiry authoritative (a database timestamp checked at read, not only a sweeper job).
4. Limit renewals. Allow zero or one extension, and tie it to a verifiable step such as entering a shipping address or starting payment authorization.
5. Move the hold later for hot items. Reserve at checkout start or payment authorization instead of add-to-cart. The tradeoff: shoppers may reach payment and find the item gone, so show availability honestly and consider a short waiting room for drops.
6. Rate limit hold creation per session, account and network, and require an account or verified contact for constrained SKUs. Rate limits alone are weak against rotating sessions, so treat them as a floor.
7. Release on signals. Release holds immediately when payment fails repeatedly, when an account is flagged, or when an owner exceeds its cap.
The edge-specific rate-limit mechanics differ by stack. See the posts on Shopify bot protection and Nginx bot protection for platform and proxy details.
Where bot detection fits: challenge and block at the cart and reserve endpoints
Application caps limit how much a bot can take. Bot detection decides whether the request deserves to reach the cap at all. You need both.
Apply detection to the narrow set of endpoints you mapped earlier: the add-to-cart call, the reservation or checkout-start call, and any cart-read route that renews a hold. Don't run the heaviest policy sitewide. Product pages and search need to stay fast and open for legitimate crawlers and shoppers. The cart and reserve endpoints are low-traffic, high-value, and rarely hit by anyone you want to keep out.
A workable policy shape: allow requests that present consistent evidence of a real browser session, challenge ambiguous ones before the hold is created, and block the ones that clearly fail. The challenge belongs before the stock decrement, not after, because a hold created and then challenged still removed inventory.
This is where Centinel Analytica fits. It tells real visitors from scrapers that look like them and applies allow, challenge or block policy through your existing CDN or edge, so your Cloudflare, CloudFront, Akamai or Fastly setup stays in place. You point the policy at the cart and reserve routes and leave the catalog open. The team behind it are former bot builders and reverse engineers, which matters here: scripted hoarding tools imitate browsers, and detection that looks at the request, browser and connection together is harder to sidestep than a single header check.
Keep a legitimate-user escape path. A challenge on the reserve endpoint should let real shoppers retry, and you should measure the effect on genuine checkouts. The post on bot false positive testing lays out how to verify visitors before you enforce.
Test the fix: a checklist for measuring held-but-unpaid inventory
A control you can't measure is a hope. Before and after any change, record the same numbers.
Baseline metrics (per constrained SKU, per drop):
- Peak held-but-unpaid units as a percentage of starting stock.
- Hold-to-paid conversion rate, by hold count and by unit count.
- Median and 95th percentile hold lifetime.
- Share of held units from owners with zero paid orders.
- Time from "sold out" display to first restock event.
- Number of renewals per hold, and the share of holds renewed more than once.
Test steps:
- Replay the previous drop's logs against the new caps in a staging environment, and compare what the shelf would have shown.
- Run a synthetic hoarder. Script a test client in your own environment that creates holds, polls to renew, and rotates sessions. Confirm each cap triggers: per-owner, per-SKU unpaid ceiling, renewal limit, and rate limit.
- Verify expiry under failure. Stop the sweeper or queue worker and confirm holds still expire at read time.
- Check the legitimate path. A real tester should complete a purchase during the same window with the policies on, including from a mobile network.
- Run in observe mode first. Log what the challenge or block policy would have done before enforcing, and review the false-positive sample.
- Watch the next real drop with a live dashboard on the metrics above and a rollback switch for each control.
Success means the unpaid share stays under your ceiling, hold lifetimes cluster at or below the TTL, and genuine conversion does not fall. If conversion falls, loosen the last control you added, not all of them.
Conclusion
Inventory hoarding is an engineering defect first and a bot problem second. If add-to-cart takes stock, renewal never expires, and anonymous sessions cost nothing, any script can empty your shelf at noon. Fix the hold lifecycle, cap unpaid stock per owner and per SKU, then put detection in front of the cart and reserve routes. Nobody should call it solved until the held-but-unpaid metric stays flat through a real drop.
If you want to see what is hitting those routes today, Centinel Analytica can help you scope allow, challenge and block policy for your cart and reserve endpoints on top of your current CDN.
Frequently asked questions
What is inventory hoarding in ecommerce?
Inventory hoarding, classified by OWASP as OAT-021 Denial of Inventory, is when bots hold limited stock or availability—such as by adding items to carts—without completing the purchase or committing to the transaction. Real shoppers then see products as unavailable. It differs from scalping, where the bot completes the purchase, and from card testing, which targets the payment step.
How do I know if bots are reserving my stock?
First confirm that add-to-cart or checkout actually holds stock in your store. Then look in hold and cart logs for near-zero gaps between expiry and re-add, fixed-interval renewals, maximum quantities on every add, single-SKU sessions, and clusters with zero paid conversion. Keep raw logs before blocking anything.
How long should a cart hold last on a limited-stock product?
There is no universal number. For scarce items many stores use roughly 5 to 15 minutes, with an absolute expiry from creation and at most one renewal tied to real progress such as starting payment. Tune it from your own conversion data and verify the expiry still works if background jobs fail.
Can rate limiting alone stop inventory hoarding?
Rate limits help but are not enough, because scripts rotate sessions and addresses to stay under per-owner thresholds. Pair them with server-side caps on held units, a ceiling on unpaid stock per SKU, limited renewals, and bot detection on the cart and reserve endpoints. [Centinel Analytica](https://centinelanalytica.com/bot-management) applies allow, challenge or block policy through your existing CDN.
Where should I put bot challenges in the checkout flow?
Place them on the endpoints that create or renew a hold: add-to-cart, reservation or checkout start, and cart reads that reset timers. Challenge before the stock decrement, since a hold created then challenged already removed inventory. Leave product pages and catalog routes open, and test the effect on genuine buyers first.
