9 min readabandoned cartsWooCommerceconversionecommerce analytics

Abandoned carts in WooCommerce: what actually gets recovered

Measured on a real WooCommerce store: the median time from search to order is 7.5 minutes, and 90% of orders land within the first hour. The abandoned-cart email arrives after the decision is made. Here is where the money is really lost, and how to read the carts already sitting in your database today.

The abandoned-cart email is the first thing anyone recommends when you say your store isn't converting. Install a plugin, schedule it for an hour later, and wait.

I measured how long a person actually takes to buy. The result puts that one-hour email in an awkward place.

This is 40,770 searches and 170 orders from a real WooCommerce collectibles store, between 10 July and 28 August 2026 — 50 days. For every order I measured the time between that person's first search and the moment the order was recorded:

From first search Orders %
Under 10 minutes 96 56%
Within the first hour 153 90%
More than 4 hours 8 4.7%

Median: seven and a half minutes. 90th percentile: 59 minutes.

Which means: by the time the abandoned-cart email goes out an hour later, nine out of ten people who were going to buy already have. It doesn't rescue them; it reminds them of something they already did. And the rest, overwhelmingly, never came back.

Why this isn't the email's fault

The abandoned-cart email isn't badly built. It's built for something else.

It works well when the purchase is expensive and considered — a sofa, a laptop, a holiday — because there people really do take two days. But in a store with a low average order value and a catalogue you can browse in five minutes, the entire decision fits inside the time it takes for the first email to go out.

Be careful about what this does and doesn't mean:

  • It doesn't mean abandoned-cart email is never worth it.
  • It does mean that before installing it you should know what time window your customers buy in. If your median is minutes too, that plugin is competing against a buy button that was already on screen.

How to measure it on your own store is further down, and it costs nothing.

The hole is before the cart

And here's the part that genuinely changes where you should put the effort.

Over those same 50 days, 2,949 sessions clicked through to a product after searching for it. People who typed what they wanted, saw results, and opened a product page. Purchase intent with no ambiguity at all.

131 ended in an order. The other 2,818 left: 95.6%.

Most of those 2,818 never added anything to a cart. And that has a consequence people skip over: they appear in no abandoned-cart report. That report only sees whoever managed to put something in. Everyone who fell earlier — the one who couldn't find their size, the one who never saw the shipping cost, the one who searched and got nothing — is invisible there.

You're staring at the last step of a staircase people fall off on the second.

Your abandoned carts are already in your database

Almost nobody mentions this, and it's the most useful thing in the article: WooCommerce already stores live carts. You don't need a plugin to see them.

They live in the wp_woocommerce_sessions table, with three columns: session_key, session_value and session_expiry. I checked this today in WooCommerce's own source, in includes/class-wc-session-handler.php. Two details that matter:

  • How long they last. The session expires after 2 days for an anonymous visitor, and after a week for someone who was logged in. After that WooCommerce deletes it. You have a window, not a history.
  • How they're stored. session_value is a serialized array, and each value inside it is serialized again. You have to unserialize twice. It's why almost everyone who opens that table by hand gives up within ten seconds.

To find out how many you have right now, one line:

SELECT COUNT(*) FROM wp_woocommerce_sessions WHERE session_value LIKE '%s:4:"cart"%';

(Swap wp_ for your own table prefix if yours is different.)

And to actually see them — what's inside, what they're worth and who they belong to — save this as wp-content/mu-plugins/live-carts.php. You can create the mu-plugins folder if it doesn't exist, and anything inside it activates on its own. It adds a new screen under the WooCommerce menu:

<?php
/**
 * Plugin Name: Live carts
 * Description: Lists the carts currently sitting in WooCommerce's session table.
 */

add_action('admin_menu', function () {
    add_submenu_page(
        'woocommerce',
        'Live carts',
        'Live carts',
        'manage_woocommerce',
        'live-carts',
        'wr_live_carts_screen'
    );
});

function wr_live_carts_screen() {
    global $wpdb;

    $rows = $wpdb->get_results(
        "SELECT session_key, session_value, session_expiry
         FROM {$wpdb->prefix}woocommerce_sessions"
    );

    $carts = [];

    foreach ($rows as $row) {
        $session = maybe_unserialize($row->session_value);
        if (!is_array($session) || empty($session['cart'])) {
            continue;
        }

        // Each session value is serialized separately: you have to unserialize twice.
        $cart = maybe_unserialize($session['cart']);
        if (!is_array($cart) || !$cart) {
            continue;
        }

        $value = 0.0;
        $lines = [];

        foreach ($cart as $item) {
            $id = !empty($item['variation_id']) ? $item['variation_id'] : ($item['product_id'] ?? 0);
            $product = $id ? wc_get_product($id) : null;
            if (!$product) {
                continue;
            }
            $qty     = (int) ($item['quantity'] ?? 0);
            $value  += (float) $product->get_price() * $qty;
            $lines[] = $qty . ' × ' . $product->get_name();
        }

        if (!$lines) {
            continue;
        }

        // session_key is the user ID when the shopper was logged in.
        $user = is_numeric($row->session_key) ? get_user_by('id', $row->session_key) : null;

        $carts[] = [
            'value'   => $value,
            'what'    => implode(', ', $lines),
            'who'     => $user ? $user->user_email : 'anonymous',
            'expires' => date_i18n('j M H:i', (int) $row->session_expiry),
        ];
    }

    usort($carts, fn($a, $b) => $b['value'] <=> $a['value']);

    echo '<div class="wrap"><h1>Live carts (' . count($carts) . ')</h1>';
    echo '<table class="widefat striped"><thead><tr>
            <th>Value</th><th>Contents</th><th>Who</th><th>Expires</th>
          </tr></thead><tbody>';

    foreach ($carts as $c) {
        printf(
            '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>',
            wc_price($c['value']),
            esc_html($c['what']),
            esc_html($c['who']),
            esc_html($c['expires'])
        );
    }

    echo '</tbody></table></div>';
}

Five minutes, zero euros, and you have the list sorted by value.

One honest warning about that list: a live cart isn't necessarily an abandoned one. It might be somebody shopping right now. What is certain is the reverse: when an order completes, WooCommerce empties the cart, so whatever is still in there is by definition everything that hasn't been bought yet.

What to do with that list

Look at it twice a week for a fortnight. Three things come out of it, and none of them needs a plugin:

  1. Which products keep showing up. A product that enters carts often and leaves as orders rarely has a specific, findable problem: the shipping, the lead time, a sold-out size, a photo.
  2. What they're worth. If your live carts add up to forty euros, this isn't your problem and you need to look further up the funnel. If it's a thousand, you know what to prioritise.
  3. Who was logged in. Those people have an email address, and those you can write to by hand. Ten hand-written emails teach you more in a week than an automated sequence does in three months, because people reply and tell you why they didn't buy.

And then the thing that actually moves the needle, going by the numbers above: if the decision happens in the first ten minutes, the fix has to live inside those ten minutes. Not in an email afterwards. Shipping cost visible before the cart, delivery time on the product page, real stock, and finding what you searched for on the first try.

How many stores actually have this set up

One piece of context I found revealing, checked today, 29 August 2026, against the WordPress.org API:

Active installs
WooCommerce 7,000,000
Cart Abandonment Recovery for WooCommerce (the most-installed abandoned-cart plugin) 300,000
Abandoned Cart Lite for WooCommerce 20,000

The most-installed plugin in the category runs on 4% of WooCommerce stores. I'm not saying that as a criticism of the plugin, which is well rated: I'm saying it because if you were convinced that everyone except you had this set up, that isn't the case. And because the other 96% have their carts sitting in the session table without knowing it.

How to measure your own decision window

The numbers above come from a single store, in collectibles, with an average order value of around €67, and they come from the sessions that went through its search — not from all its traffic. Your store will be different, which is exactly why it's worth measuring yours instead of taking mine on faith.

The minimum you need: the time of each customer's first interaction and the time of their order. Another ten-line mu-plugin that stamps each logged-in user's first visit gets you there:

<?php
/**
 * Plugin Name: First visit
 * Description: Stamps when each logged-in user's shopping session began.
 */

add_action('wp', function () {
    if (is_admin() || !is_user_logged_in()) {
        return;
    }

    $key = 'wr_first_visit_' . gmdate('Y-m-d');
    if (get_user_meta(get_current_user_id(), $key, true) === '') {
        update_user_meta(get_current_user_id(), $key, current_time('mysql', true));
    }
});

Leave it a month, then cross that stamp with the orders. On modern WooCommerce, orders live in the wp_wc_orders table:

SELECT
  COUNT(*) AS orders,
  ROUND(AVG(TIMESTAMPDIFF(MINUTE, um.meta_value, o.date_created_gmt)), 1) AS mean_min,
  SUM(TIMESTAMPDIFF(MINUTE, um.meta_value, o.date_created_gmt) <= 60) AS within_1h
FROM wp_wc_orders o
JOIN wp_usermeta um
  ON um.user_id = o.customer_id
 AND um.meta_key = CONCAT('wr_first_visit_', DATE(o.date_created_gmt))
WHERE o.status IN ('wc-processing', 'wc-completed')
  AND o.date_created_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 60 DAY);

(If your store is older and doesn't use the new order storage, swap wp_wc_orders o for wp_posts o with o.post_type = 'shop_order', use o.post_date_gmt instead of o.date_created_gmt, and take customer_id from the _customer_user meta. And swap wp_ for your prefix if yours is different.)

What matters there is within_1h against orders. If most of your orders land inside the first hour, the abandoned-cart email scheduled for an hour later arrives late by design on your store, just as it does on this one.

Two warnings so you don't fool yourself with this number: it only counts logged-in customers — guest checkouts leave no stamp — and you want the median more than the mean, because four stragglers who buy the next day blow the mean up. You can see it in the figures above: mean 56.8 minutes, median 7.5.


In order, what I'd do:

  1. Paste in the live-carts mu-plugin. It's a list you already had and weren't looking at.
  2. Read it twice a week for a fortnight. Note which products keep repeating.
  3. Measure your decision window. If your median is minutes, stop investing in the email sequence and fix what happens on screen.
  4. Go up a step. The abandoned-cart report only sees people who added something. On this store, for every person who ended up buying there were 21 who had opened a product page and left before that. That step is bigger, and almost nobody looks at it.

That last point is the one that surprises people once it's in numbers: of 2,949 sessions with clear intent, 2,818 left without reaching the cart. A good share of that drop starts at the dumbest possible place — not finding what you searched for — and that one is measurable and fixable. There's the full guide on how to choose an ecommerce search engine, with each option's prices verified, and how much your ecommerce search is costing you, using the order data from this same store. And if you want to see where these numbers come from, that's WildRock.

Is your store losing sales to searches that find nothing?

WildRock adds a search to your WooCommerce that understands what shoppers mean, and tells you in euros how much it generates. Free plugin, no code.