A cart recovery system on AWS for a few dollars a month
A small online store loses more sales to second thoughts than to anything else. The shopper who filled a cart with three things and then got distracted by a phone call. The one who got to the shipping page and decided to “come back later.” The one who was just price-checking and never meant to buy yet but really might. None of them are lost causes — most of them simply forgot. This post walks through the design of a small system that notices a cart was left behind, waits a sensible amount of time, and sends one friendly reminder with what they left — and stops the second they buy or ask to be left alone.
Key takeaways
- Three sources for carts: a storefront webhook, a nightly Drive export a human can read, and a saved-link tag lane.
- Every cart ends in one of four moves on each wake-up: still shopping, first reminder, second reminder, or give up.
- Per-size waits: a small cart waits 4 hours then 24; a big cart waits 1 hour then 20.
- Reminders are friendly, respect quiet hours, carry the items and a return link, and stop on checkout.
- Designed on AWS for about $2.20/month at typical small-business volume.
The whole system on one page
Before any code, here’s the shape of what we’re designing.
What you set up once (the outside)
- Carts. Your storefront posts a cart event to a small webhook the moment a shopper adds an item, changes the cart, or checks out. Each event carries the cart id, the shopper’s email (once they’ve entered it), the items, the cart total, and a timestamp. That’s the main lane. Two others, covered in Part 2, fill the gaps — a nightly Drive export (the day’s carts land in a sheet a human can scan) and a saved-link tag lane (carts that start from a saved link the shopper emailed themselves get marked so the reminder copy can match).
- A rules folder. Two short Google Docs in a Drive folder. The rules doc covers the wait for each cart size — how long to wait before the first reminder, and how long before the second. A small cart might wait 4 hours, then 24; a big cart waits 1 hour, then 20, because a bigger basket is worth a faster, gentler nudge. The doc also sets the quiet hours (no sends between 9pm and 8am local by default), the give-up point (two reminders, then stop), and the do-not-disturb rule (skip anyone who got a reminder in the last few days). The voice doc holds one reminder template per step — the warm first nudge and the slightly warmer second.
- Shoppers. The people who left a cart behind. Reminders land in their email inbox with the items they left, the cart total, a return link straight back to the filled cart, and a one-click unsubscribe. Nobody who unsubscribes ever hears from the system again.
What runs per cart (the inside)
- The cart intake. Three sources feed the list. The storefront webhook is the main one — the moment a cart changes, a small Function URL Lambda writes or updates one row per cart in DynamoDB and schedules a wake-up for when the first reminder might be due. New carts also arrive via the nightly Drive export (the day’s carts mirrored to a sheet for a human to read) and the saved-link tag lane (carts started from a saved link get a small flag so the copy can say “still saving these?” instead of “you left these behind”).
- The waiter. Wakes once per cart, at the time the intake scheduled. Reads the cart. Computes time-since-abandon. Compares against the per-size wait in the rules doc. Picks one of four moves. Still shopping: the cart changed again or hasn’t waited long enough — do nothing, reschedule. First reminder: crossed the first wait with no checkout — send a friendly nudge with the items and a return link. Second reminder: crossed the second wait, still no checkout — send one slightly warmer follow-up. Give up: two reminders sent and still no checkout — stop; the cart is logged and left alone. The waiter doesn’t call a model to decide — the move logic is plain Python.
- The sender. Reads the voice doc, fills the reminder template for the chosen move, and asks Bedrock Haiku 4.5 to polish just the opening line into the store’s voice (with a plain fallback if it’s unavailable). Email goes through SES outbound. It honors quiet hours and the do-not-disturb list. Every send writes a row to DynamoDB so the next wake-up knows a reminder already went out. A monthly summary writes a short paragraph: carts seen, reminders sent, carts recovered, dollars won back.
In plain words
A shopper fills a cart with $120 of goods and gets to the shipping page, then closes the tab to take a call. An hour passes (the cart is on the bigger side, so the wait is short). The system emails them: “Still thinking it over? Your cart’s still here — the linen shirt, the two candles, and the tote, $120 in all. [return to cart]” with a one-click unsubscribe at the foot. They don’t open it that evening. Twenty hours later, one gentle follow-up: “Last nudge — we saved your cart in case you still want it.” This time they click through and check out. The moment that checkout event lands, the system clears the chain — no third email, no nagging. If they’d unsubscribed instead, that would have stopped everything too.
The cost of running this is about $2.20 a month at SMB volume. The cost of not running it is every full cart that quietly evaporates because nobody ever reminded the shopper it was still there.
Design rules that shaped every decision
- Every reminder ships with full context — the items, the total, a return link, an unsubscribe. The shopper never has to dig.
- Four moves, always. Still shopping, first reminder, second reminder, give up. There is no fifth.
- At most two reminders, ever. One gentle nudge, not a barrage. The give-up point is a hard stop.
- Quiet hours and the do-not-disturb list are respected. A reminder is a finite resource; bad timing burns it.
- Checkout stops the chase on the next wake-up. The system never nags a cart that is already bought.
- Every send and every stop is logged. Look back next month and you can see exactly what went out and why.
Why this shape
Most small stores handle abandoned carts in one of three ways: a plugin that blasts everyone the same three emails on the same schedule, a vague plan to “set something up one day,” or nothing at all. The plugin works until it annoys people — three emails for a $9 cart is how you train shoppers to mark you as spam. The plan never happens. And nothing at all is how a store leaves real money on the table every single day.
The setup above keeps the cart data where your store already writes it, but adds a small system that wakes up per cart and acts only when a reminder is genuinely worth sending. Reminders go out after a sensible wait, sized to the cart. They are friendly by default and never escalate into pressure. They carry a return link so finishing the purchase is one click. They stop the moment the shopper buys, and the moment anyone asks to be left alone. The system is invisible on the carts that come back on their own; it only shows up for the ones that would otherwise have been forgotten.
The next four posts walk through each piece in turn: how an abandoned cart gets spotted, how a cart reminder gets timed, how a cart reminder reaches the shopper, and how a cart recovery stops on checkout. One diagram per post. A cost breakdown and a final engineering reference at the end.
All posts