twimco.Let’s talk ↗︎
Menu
Field notes / Commerce

The inventory hold expired while they were still checking out

They had the item. Payment was open. The hold timer died first. Another buyer took the unit, and checkout discovered sold out after payment authorization had already started—a race you can rehearse before the next drop.

Editorial engraving of a sunlit retail loft where a shopper pays on a tablet beside an empty sand glass as another shopper lifts the last boxed unit.

Composite pattern: inventory holds that expire before realistic checkout completion create sold-out-after-pay races. Set TTL from measured checkout time, extend on progress, reserve before authorization, and fail closed with a clear customer path. Treat the race as a launch checklist item, not a freak support ticket.

What broke

Composite post-mortem, details changed: a limited-drop storefront held inventory when the shopper reached checkout. The hold TTL was shorter than the median time to complete payment details, address validation, and 3-D Secure. The hold expired while the customer was still on the payment step. Another shopper purchased the last unit. Checkout returned sold out after card authorization had already started. Support spent the evening explaining a race the architecture invented.

This is labeled composite: the pattern is common across flash drops, ticketed events, and low-stock heroes. The inventory hold expired while they were still checking out.

The timer itself had a history. Someone set it to five minutes during a launch two years earlier, when checkout had no address validation and no bank challenge step. Both were added later, each sensible on its own. Nobody went back to the timer.

Timeline of the race

T0: add to cart creates a soft hold. T1: customer enters shipping. T2: hold TTL ends; inventory returns to pool. T3: second customer checks out successfully. T4: first customer submits payment. T5: authorization succeeds or pending; reservation fails; UI shows sold out; refund or void path begins—sometimes after the customer saw a success spinner.

If payment capture can start without a live hold, you will charge people for units you cannot ship. If the hold can die silently mid-checkout, in stock was theater.

Notice who loses. Not the slow shopper in general—the shopper on a weak mobile connection, the one whose bank sent a 3-D Secure challenge to another device, the one who stopped to fix an address the validator rejected. The race punishes exactly the people doing what checkout asked of them.

Sequence of work

Hold-expiry race (composite)

  1. Hold created

    Checkout starts; unit reserved on a TTL.

  2. Customer still paying

    Address and 3-D Secure still in progress.

  3. TTL ends

    Unit returns to the sellable pool.

  4. Second buyer wins

    Another checkout consumes the unit.

  5. First buyer fails

    Auth path meets sold out; void/refund begins.

Illustrative composite timeline; not a client screenshot or measured result.
If the hold can die silently mid-checkout, in stock was theater.

Root causes to name

TTL shorter than realistic checkout duration. Hold not extended on payment-step heartbeat. Shared inventory across channels without a single reservation authority. Oversell flags used as a substitute for holds. Mobile clients with slow networks hitting expiry more often. No customer-visible countdown or refresh when the hold is near death.

None of these are exotic. They are defaults that survive because happy-path load tests never sit on the payment form for four minutes.

Two more show up in nearly every review of this kind. Hold events were not logged, so the team rebuilt the timeline from payment-processor records and guesswork. And bundles reserved each SKU on its own timer, so a kit could lose one component while the rest stayed held. Log hold create, extend, release, and consume as first-class events, and expire or extend a multi-SKU reservation as one set.

What to change

Set hold TTL from measured checkout percentiles, not from a round number someone liked. Pull the time from checkout start to authorization request for the last few drops, look at the slow tail rather than the median, and set the hold past it with margin. Re-measure whenever a step is added or removed—a new wallet, a new fraud check, a new address service.

Extend holds on meaningful checkout progress: entering a valid address, opening the payment step, starting a bank challenge. Keep a single reservation service for web, app, and marketplaces when they share stock, and account for marketplace sync lag so a channel that updates every few minutes cannot sell a unit your site still holds.

Fail closed before authorization if the hold is gone. That check belongs immediately before the payment call, not after it. Show the customer a clear refresh-or-lose state when time is low, with countdown and sold-out messages that work with screen readers and do not rely on color alone.

For ecommerce drops, rehearse the race in staging with two clients and a short TTL on purpose until the loser path is kind and correct. The race is easy to reproduce once you decide to. In staging, drop the hold to sixty seconds, stock one unit, and open two checkouts on two devices. Let the first sit on the payment step past the timer while the second buys. Then watch what the first shopper sees, what the payment processor records, what the inventory log says, and what lands in the support queue.

Run it again with a bank challenge on the first device, again with a bundle, and again with the marketplace channel selling the same unit. Each run should end in one of two outcomes: the first shopper's hold was extended and they got the item, or they were stopped before authorization with a clear message. Any run that ends with a charge and a sold-out screen is a launch blocker.

Keep the script. Run it whenever the checkout flow changes, and make it a line item on the drop checklist so it is not left to whoever remembers the last incident. Share the recording of each run with support, so the people who will answer the angry emails have already seen exactly what the losing shopper sees on screen.

Decision guide

Short TTL vs progress-aware hold

ConsiderationFixed short TTLProgress-aware reservation
BasisRound number in config.Measured checkout percentiles.
During paymentHold may die silently.Extend on meaningful steps.
Before authCharge can start without reservation.Fail closed if hold is gone.
Customer signalSurprise sold out.Countdown or refresh-or-lose.
Launch testHappy path only.Two-client race rehearsal.
Decision framework: assess against your checkout timing; not client data.

Customer and finance aftermath

Define whether an authorization without reservation auto-voids, how fast the customer is notified, and whether they get a waitlist or goodwill path. Finance and support need the same incident ID the inventory log carries. Do not make CS invent the story. Give support a short lookup—order or email in, hold and payment events out—so an agent can confirm in one screen whether a complaint is a race or a true stockout.

Write the customer message before the drop, not during the incident. It should say plainly that the item sold out while they were paying, that the authorization has been voided, when the hold will disappear from their statement, and what they can do next—a waitlist, a restock alert, or a code if your policy allows one. A customer who watched a success spinner deserves a specific answer, not "we are looking into it."

Put this into practice

Pull a week of sold-out-at-payment events. Tag hold-expiry races separately from true stockouts. Fix TTL and pre-auth checks before the next drop. Add the race to the launch checklist beside payment capture timing and inventory sync checks, and name one owner for hold behavior across web, app, and marketplace channels.

We build checkout systems where inventory promises last through payment. If your holds die mid-checkout—talk to us.

Related work: Paddle8.

twimco.

We build alongside the people doing the work.
Bring us the problem you’re thinking about.

Related work

Paddle8 ↗︎