Everyone Adds to Cart, Few Check Out
A flash sale is the same contended inventory as ticketing, but the winning design is the opposite: oversubscribe the cart and decrement only at checkout. A runnable lab measuring the funnel, a payment-TTL stock reclaim, and a per-user anti-scalper cap.
A flash sale is the same contended, finite inventory as selling tickets — but the correct design is the opposite of a seat map. A ticket seat is reserved the moment it's in your cart; a flash-sale item is not, and that's on purpose. You've felt it: at 10:00 sharp everyone can add the item to their cart, but most people hit "sold out" at checkout. That's not a bug — it's the right architecture, and here's why, measured.
Runnable companion:
ecommerce-flash-saleon GitHub.make funnel && make timeout && make limitreproduces every number below (pure Python, no deps). Sibling:high-demand-ticketing.
Decrement at checkout, not in the cart
The key decision is when stock leaves inventory. Reserve-at-cart (ticketing style) decrements on add-to-cart; deduct-at-checkout (flash-sale style) touches nothing until the order is placed. With 400 shoppers, 100 units, as cart abandonment rises:
abandon strategy add-to-cart sold wasted
20% reserve-at-cart 25% 80 20
deduct-at-checkout 100% 100 0
40% reserve-at-cart 25% 60 40
deduct-at-checkout 100% 100 0
60% reserve-at-cart 25% 40 60
deduct-at-checkout 100% 100 0
reserve-at-cart locks a unit for every abandoner, so the cart "sells out" to holds — only 25% of shoppers even get an item into the cart, and at 40% abandonment it sells just 60 of 100 units. deduct-at-checkout keeps 100% of stock available to buyers who actually convert, so it sells the full 100 at every abandonment rate. "Everyone adds to cart, few check out" is the design working: it refuses to waste stock on carts that will be abandoned.
Pending orders need a payment TTL
Deducting at checkout leaves a smaller leak: a buyer places the order (stock −1, pending) then never pays. A TTL cancels the unpaid order and returns the unit to waiting demand:
non-payment no timeout: sold with timeout: sold
10% 90 (90%) 100 (100%)
30% 70 (70%) 100 (100%)
50% 50 (50%) 100 (100%)
At 30% non-payment you sell only 70% of stock without a TTL; the timeout brings it back to ~100%. It's the seat-hold + reaper pattern from ticketing, moved to the order level.
A per-user cap stops scalpers
Flash sales attract bots — a few accounts firing many requests each. 100 units, 200 genuine buyers (one request) + 20 scalpers (50 requests each):
policy to scalpers unique buyers most by one
no limit 83 (83%) 36 8.1
per-user limit = 1 20 (20%) 100 1.0
With no cap, stock goes to whoever sends the most requests — scalpers scoop 83% and only ~36 people are served. A one-per-account cap drops that to 20% and spreads the drop across all 100 unique buyers. But a cap on accounts is only as strong as account identity.
A cap on users isn't a cap on bots
A bot creates thousands of free fake accounts (a Sybil attack), so a per-account cap barely dents it — you need defense in depth, where each layer spends a different scarce resource the bot has to have. Scalper share of 100 units, adding one layer at a time (bot: unlimited fake accounts, 15 real cards, 85% datacenter IPs):
defense (cumulative) to scalpers real buyers blocked
no defense 94% 0
+ per-user cap 94% 0
+ datacenter/ASN block 69% 0
+ payment/card cap 15% 0
+ account-age gate 0% 25
Per-user cap alone ≈ no defense (fake accounts are free). Blocking datacenter/proxy IPs cuts most bot traffic; a per-card cap pins the bot to its few real cards (the cheap decisive layer); an account-age gate stops brand-new Sybil accounts entirely — but locks out 25 real first-time buyers, so it's a trade-off. Anchor on what bots can't cheaply mass-produce (cards, phone numbers, aged accounts, non-datacenter IPs); it's an economics game — raise the attacker's cost above their profit, and cancel-and-refund suspicious orders after the fact.
What carries over from ticketing
The checkout itself still needs the mechanics proven in the ticketing lab, unchanged: an atomic decrement so a burst of orders never oversells (buying N units is one all-or-nothing update), idempotent payment on a client key, and a virtual waiting room for backpressure when the spike is large. The one real difference is risk appetite: ticketing must never oversell; e-commerce often tolerates a small deliberate oversell (backorder / refund) to lift conversion — a product decision, not a correctness one.
What I'd say in an interview
- Reserve stock in the cart only for unique, rarely-abandoned units (a seat). For fungible goods with high abandonment, oversubscribe the cart and decrement at checkout — you sell the full stock instead of wasting it on abandoners (measured: 60 → 100 of 100 units at 40% abandonment).
- Put a TTL on the pending order so non-payers don't strand inventory — the reaper pattern at the order level (70% → ~100% utilization).
- A per-user cap alone is beaten by fake accounts (Sybil) — stack defense in depth (datacenter/ASN block, per-card cap, account-age gate), anchoring on resources bots can't mass-produce; each layer has a cost or a gap.
- The checkout decrement is still atomic and payment idempotent — same invariants as ticketing; only the reservation point and the oversell tolerance change.