Repair WooCommerce core: database bloat and maintenance
The WooCommerce session table that grows without bound
Every shopper who opens your store, whether they buy or not, gets a row in wp_woocommerce_sessions. WooCommerce is supposed to delete those rows once they expire. On plenty of stores that cleanup quietly stops working, and the table just keeps growing, slowing down every page that touches it. Here is why the cleanup breaks and a small script that clears it back down safely, without touching a shopper who is checking out right now.
The sessions table only shrinks when a scheduled cleanup event actually fires, and on many stores it stops firing without anyone noticing. Run a small Python or Node.js job on a schedule that reads the table's real size from the WooCommerce REST API's system status report, checks Stripe to make sure no shopper is mid-checkout right now, and only then calls WooCommerce's own clear_sessions maintenance tool. Full code, tests, and a dry run guard are below.
The problem in plain words
WooCommerce needs somewhere to remember a shopper's cart between page loads, and that place is a database table called wp_woocommerce_sessions. Every visitor gets a row, whether they are logged in or not, whether they buy anything or not. Each row carries an expiry time, usually about two days out.
WooCommerce is meant to sweep out expired rows on its own, on a schedule, through WordPress's background task system. When that sweep stops happening, nothing deletes the old rows. New visitors keep adding new ones. The table only ever grows, and every query that touches it, including the one that loads a cart on every single page view, gets slower as it grows.
Why it happens
The cleanup that keeps this table small runs through WordPress's own scheduled task system, and that system depends on real page visits or a real server cron to keep ticking. A few common reasons it quietly stops:
- WP-Cron is disabled (many performance guides recommend this) and nothing replaced it with a real system cron entry, so scheduled WooCommerce events, including session cleanup, never fire.
- The background task queue (Action Scheduler) has a backlog of stuck or failed jobs, so new cleanup runs get pushed further and further behind.
- The cleanup query itself times out on a very large table before it finishes, so the table stays large enough to keep timing out on every future attempt.
- A caching or hosting setup serves pages from a cache so aggressively that the visits which would normally trigger WP-Cron's check almost never reach WordPress at all.
This is a well known WooCommerce complaint. Store owners have reported this single table reaching hundreds of megabytes, and in extreme cases several gigabytes, almost entirely expired rows that were simply never swept out. See the citations at the end for real reports of the size this can reach.
WooCommerce already ships the fix. There is a REST reachable maintenance tool, clear_sessions, built for exactly this table. The hard part is not clearing it, it is knowing when it is safe to clear it, because a blunt run can drop a real shopper's cart mid checkout. A small job that checks size first and checks for an active checkout second turns a risky manual action into a routine, scheduled one.
The fix, as a flow
We do not touch the live checkout path. We add a job that runs every so often, reads the sessions table's real size from WooCommerce's own system status report, and only if it has crossed a size we choose, checks Stripe for any recent order whose payment still looks open. If nobody looks like they are actively paying right now, it calls the built in cleanup tool and the table drops back down.
Build it step by step
Get access to both systems
You need a Stripe secret key and a WooCommerce REST API key pair (a consumer key and a consumer secret) with read access to orders and manage access to system status, since the tool that clears sessions lives under system status. Create the WooCommerce key under WooCommerce, Settings, Advanced, REST API. Keep every value in environment variables, never in the file.
pip install stripe requests
export STRIPE_SECRET_KEY="sk_live_..."
export WOO_STORE_URL="https://yourstore.com"
export WOO_CONSUMER_KEY="ck_..."
export WOO_CONSUMER_SECRET="cs_..."
export MAX_SESSIONS_MB="50"
export DRY_RUN="true" # start safe, change to false to write
npm install stripe
export STRIPE_SECRET_KEY="sk_live_..."
export WOO_STORE_URL="https://yourstore.com"
export WOO_CONSUMER_KEY="ck_..."
export WOO_CONSUMER_SECRET="cs_..."
export MAX_SESSIONS_MB="50"
export DRY_RUN="true" // start safe, change to false to write
Read the sessions table's real size
WooCommerce's system status report already knows the size of every one of its database tables, in megabytes, split into data and index. Ask the REST API for it and pull out the row for woocommerce_sessions. This is the same number a shop owner would see if they read the report by hand, just automated.
import requests
from requests.auth import HTTPBasicAuth
WOO_URL = "https://yourstore.com"
AUTH = HTTPBasicAuth("ck_...", "cs_...")
def sessions_table_size_mb(system_status):
tables = ((system_status.get("database") or {}).get("database_tables") or {}).get("other") or {}
row = tables.get("woocommerce_sessions") or tables.get("wp_woocommerce_sessions") or {}
return float(row.get("data", 0) or 0) + float(row.get("index", 0) or 0)
def get_system_status():
r = requests.get(f"{WOO_URL}/wp-json/wc/v3/system_status", auth=AUTH, timeout=30)
r.raise_for_status()
return r.json()
const WOO_URL = "https://yourstore.com";
const AUTH = "Basic " + Buffer.from("ck_...:cs_...").toString("base64");
export function sessionsTableSizeMb(systemStatus) {
const tables = systemStatus?.database?.database_tables?.other || {};
const row = tables.woocommerce_sessions || tables.wp_woocommerce_sessions || {};
return Number(row.data || 0) + Number(row.index || 0);
}
async function getSystemStatus() {
const res = await fetch(`${WOO_URL}/wp-json/wc/v3/system_status`, {
headers: { Authorization: AUTH },
});
if (!res.ok) throw new Error(`system_status returned ${res.status}`);
return res.json();
}
Check for a checkout that might still be in progress
Clearing sessions is blunt. It drops every active cart along with the expired ones, so before we act we look at orders created in the last few minutes that are still pending, on hold, or a checkout draft, and read each one's saved Stripe PaymentIntent id from order meta _stripe_intent_id (or transaction_id as a fallback). If Stripe says any of those intents are still open, somebody may be mid checkout right now.
import time, stripe
OPEN_INTENT_STATUSES = {"requires_action", "requires_confirmation", "processing", "requires_payment_method"}
LIVE_ORDER_STATUSES = {"pending", "on-hold", "checkout-draft"}
def intent_id_of(order):
for meta in order.get("meta_data") or []:
if meta.get("key") == "_stripe_intent_id" and meta.get("value"):
return meta["value"]
tid = order.get("transaction_id")
return tid if tid and tid.startswith("pi_") else None
def recent_live_orders(guard_minutes):
after = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(time.time() - guard_minutes * 60))
r = requests.get(
f"{WOO_URL}/wp-json/wc/v3/orders",
params={"status": ",".join(LIVE_ORDER_STATUSES), "after": after, "per_page": 50},
auth=AUTH, timeout=30,
)
r.raise_for_status()
return r.json()
const OPEN_INTENT_STATUSES = new Set([
"requires_action", "requires_confirmation", "processing", "requires_payment_method",
]);
const LIVE_ORDER_STATUSES = ["pending", "on-hold", "checkout-draft"];
export function intentIdOf(order) {
for (const meta of order.meta_data || []) {
if (meta.key === "_stripe_intent_id" && meta.value) return meta.value;
}
const tid = order.transaction_id;
return tid && tid.startsWith("pi_") ? tid : null;
}
async function recentLiveOrders(guardMinutes) {
const after = new Date(Date.now() - guardMinutes * 60000).toISOString();
return woo(`/orders?status=${LIVE_ORDER_STATUSES.join(",")}&after=${after}&per_page=50`);
}
Decide, with one pure function
Keep the decision in its own function that takes the table size, the threshold, and a count of open checkouts, and returns an action. A pure function like this is easy to read and easy to test, which we do later. The rule is simple. If the table is not actually bloated, skip it. If it is bloated but someone looks like they are checking out, wait. Otherwise, clear it.
def decide(sessions_size_mb, threshold_mb, open_checkout_count):
if sessions_size_mb < threshold_mb:
return ("skip", "sessions table is under the size threshold")
if open_checkout_count > 0:
return ("wait", "a checkout looks in progress, wait for it to settle")
return ("clear", "sessions table is bloated and no checkout is in progress")
export function decide(sessionsSizeMb, thresholdMb, openCheckoutCount) {
if (sessionsSizeMb < thresholdMb) return ["skip", "sessions table is under the size threshold"];
if (openCheckoutCount > 0) return ["wait", "a checkout looks in progress, wait for it to settle"];
return ["clear", "sessions table is bloated and no checkout is in progress"];
}
Clear sessions the way WooCommerce itself would
When the action is clear, call WooCommerce's own maintenance tool, the same one listed under WooCommerce, Status, Tools in the admin. It is reachable through the REST API as a system status tool, so the same safe request that a shop manager would trigger by hand is the one our script sends.
def clear_sessions():
requests.put(
f"{WOO_URL}/wp-json/wc/v3/system_status/tools/clear_sessions",
auth=AUTH, timeout=60,
).raise_for_status()
async function clearSessions() {
await woo("/system_status/tools/clear_sessions", { method: "PUT" });
}
Wire it together with a dry run guard
The run function ties every piece together. Notice the dry run guard. On the first few runs, leave DRY_RUN on so the script only reports the table's size, the checkout guard result, and what it would do. Read the output, trust it, then switch it off to let it write. Run it on a schedule with cron, once an hour is usually plenty.
Always start with DRY_RUN=true. Clearing sessions removes real, active carts along with the expired ones, so you want to see the size and the checkout guard's answer before it ever runs for real.
The full code
Here is the complete job in one file for each language. It reads settings from the environment, logs what it does, respects the dry run flag, and never clears the table without first checking that nobody looks like they are mid checkout.
View this code on GitHub Full runnable folder with tests in the woocommerce-fixes repo.
"""Clear a bloated wp_woocommerce_sessions table, without cutting off a live checkout.
WooCommerce is supposed to prune expired session rows on its own, every time a
scheduled cleanup event runs. When that event stops firing (WP-Cron disabled, Action
Scheduler stuck, a host that kills long requests), expired rows never get removed and
the table grows without bound. Some stores have reported this table alone reaching
several gigabytes, almost all of it expired rows.
WooCommerce ships a REST-reachable maintenance tool that empties the sessions table:
PUT /wp-json/wc/v3/system_status/tools/clear_sessions. It is effective but blunt, it
wipes every session, including a shopper who is mid-checkout right now. So before we
run it we check Stripe for any PaymentIntent created in the last few minutes that is
still open (requires_action, processing, or requires_payment_method), using the
PaymentIntent id saved on the matching WooCommerce order's _stripe_intent_id meta (or
transaction_id as a fallback). If anyone looks like they are actively paying, we wait.
Safe by default. Run on a schedule.
"""
import os
import time
import logging
import stripe
import requests
from requests.auth import HTTPBasicAuth
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("clear_stale_sessions")
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
WOO_URL = os.environ["WOO_STORE_URL"].rstrip("/")
AUTH = HTTPBasicAuth(os.environ["WOO_CONSUMER_KEY"], os.environ["WOO_CONSUMER_SECRET"])
MAX_SESSIONS_MB = float(os.environ.get("MAX_SESSIONS_MB", "50"))
CHECKOUT_GUARD_MINUTES = int(os.environ.get("CHECKOUT_GUARD_MINUTES", "15"))
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"
OPEN_INTENT_STATUSES = {"requires_action", "requires_confirmation", "processing", "requires_payment_method"}
LIVE_ORDER_STATUSES = {"pending", "on-hold", "checkout-draft"}
def intent_id_of(order):
"""The saved Stripe PaymentIntent id, from meta _stripe_intent_id or transaction_id."""
for meta in order.get("meta_data") or []:
if meta.get("key") == "_stripe_intent_id" and meta.get("value"):
return meta["value"]
tid = order.get("transaction_id")
return tid if tid and tid.startswith("pi_") else None
def sessions_table_size_mb(system_status):
"""Read the sessions table's data + index size, in MB, from a system_status payload."""
tables = ((system_status.get("database") or {}).get("database_tables") or {}).get("other") or {}
row = tables.get("woocommerce_sessions") or tables.get("wp_woocommerce_sessions") or {}
return float(row.get("data", 0) or 0) + float(row.get("index", 0) or 0)
def decide(sessions_size_mb, threshold_mb, open_checkout_count):
"""Pure decision: should we clear the sessions table right now?
sessions_size_mb -- current size (data + index, MB) of wp_woocommerce_sessions
threshold_mb -- size at which the table counts as bloated
open_checkout_count -- number of recent orders with a Stripe PaymentIntent that is
still open (a shopper who may be mid-checkout right now)
"""
if sessions_size_mb < threshold_mb:
return ("skip", "sessions table is under the size threshold")
if open_checkout_count > 0:
return ("wait", "a checkout looks in progress, wait for it to settle")
return ("clear", "sessions table is bloated and no checkout is in progress")
def get_system_status():
r = requests.get(f"{WOO_URL}/wp-json/wc/v3/system_status", auth=AUTH, timeout=30)
r.raise_for_status()
return r.json()
def recent_live_orders(guard_minutes):
after = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(time.time() - guard_minutes * 60))
r = requests.get(
f"{WOO_URL}/wp-json/wc/v3/orders",
params={"status": ",".join(LIVE_ORDER_STATUSES), "after": after, "per_page": 50},
auth=AUTH, timeout=30,
)
r.raise_for_status()
return r.json()
def get_intent(intent_id):
if not intent_id:
return None
try:
return stripe.PaymentIntent.retrieve(intent_id)
except stripe.error.InvalidRequestError:
return None
def count_open_checkouts(guard_minutes):
open_count = 0
for order in recent_live_orders(guard_minutes):
intent = get_intent(intent_id_of(order))
if intent is not None and intent.get("status") in OPEN_INTENT_STATUSES:
open_count += 1
return open_count
def clear_sessions():
requests.put(
f"{WOO_URL}/wp-json/wc/v3/system_status/tools/clear_sessions",
auth=AUTH, timeout=60,
).raise_for_status()
def run():
status = get_system_status()
size_mb = sessions_table_size_mb(status)
open_checkouts = count_open_checkouts(CHECKOUT_GUARD_MINUTES)
action, reason = decide(size_mb, MAX_SESSIONS_MB, open_checkouts)
if action == "skip":
log.info("Sessions table is %.1f MB, under the %.1f MB threshold. Nothing to do.", size_mb, MAX_SESSIONS_MB)
return
if action == "wait":
log.warning(
"Sessions table is %.1f MB (over %.1f MB) but %d checkout(s) look in progress. %s",
size_mb, MAX_SESSIONS_MB, open_checkouts, reason,
)
return
log.info("Sessions table is %.1f MB (over %.1f MB) and no checkout is in progress. %s",
size_mb, MAX_SESSIONS_MB, "Would clear it." if DRY_RUN else "Clearing it now.")
if not DRY_RUN:
clear_sessions()
log.info("Cleared wp_woocommerce_sessions.")
if __name__ == "__main__":
run()
/**
* Clear a bloated wp_woocommerce_sessions table, without cutting off a live checkout.
*
* WooCommerce is supposed to prune expired session rows on its own, every time a
* scheduled cleanup event runs. When that event stops firing (WP-Cron disabled, Action
* Scheduler stuck, a host that kills long requests), expired rows never get removed and
* the table grows without bound. Some stores have reported this table alone reaching
* several gigabytes, almost all of it expired rows.
*
* WooCommerce ships a REST-reachable maintenance tool that empties the sessions table:
* PUT /wp-json/wc/v3/system_status/tools/clear_sessions. It is effective but blunt, it
* wipes every session, including a shopper who is mid-checkout right now. So before we
* run it we check Stripe for any PaymentIntent created in the last few minutes that is
* still open (requires_action, processing, or requires_payment_method), using the
* PaymentIntent id saved on the matching WooCommerce order's _stripe_intent_id meta (or
* transaction_id as a fallback). If anyone looks like they are actively paying, we wait.
*
* Safe by default. Run on a schedule.
*
* Guide: https://www.allanninal.dev/woocommerce/session-table-balloons/
*/
import Stripe from "stripe";
import { pathToFileURL } from "node:url";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "sk_test_dummy");
const WOO_URL = (process.env.WOO_STORE_URL || "https://example.com").replace(/\/$/, "");
const AUTH = "Basic " + Buffer.from(
`${process.env.WOO_CONSUMER_KEY || "ck_dummy"}:${process.env.WOO_CONSUMER_SECRET || "cs_dummy"}`
).toString("base64");
const MAX_SESSIONS_MB = Number(process.env.MAX_SESSIONS_MB || 50);
const CHECKOUT_GUARD_MINUTES = Number(process.env.CHECKOUT_GUARD_MINUTES || 15);
const DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";
const OPEN_INTENT_STATUSES = new Set([
"requires_action", "requires_confirmation", "processing", "requires_payment_method",
]);
const LIVE_ORDER_STATUSES = ["pending", "on-hold", "checkout-draft"];
export function intentIdOf(order) {
for (const meta of order.meta_data || []) {
if (meta.key === "_stripe_intent_id" && meta.value) return meta.value;
}
const tid = order.transaction_id;
return tid && tid.startsWith("pi_") ? tid : null;
}
export function sessionsTableSizeMb(systemStatus) {
const tables = systemStatus?.database?.database_tables?.other || {};
const row = tables.woocommerce_sessions || tables.wp_woocommerce_sessions || {};
return Number(row.data || 0) + Number(row.index || 0);
}
/**
* Pure decision: should we clear the sessions table right now?
*
* sessionsSizeMb -- current size (data + index, MB) of wp_woocommerce_sessions
* thresholdMb -- size at which the table counts as bloated
* openCheckoutCount -- number of recent orders with a Stripe PaymentIntent that is
* still open (a shopper who may be mid-checkout right now)
*/
export function decide(sessionsSizeMb, thresholdMb, openCheckoutCount) {
if (sessionsSizeMb < thresholdMb) return ["skip", "sessions table is under the size threshold"];
if (openCheckoutCount > 0) return ["wait", "a checkout looks in progress, wait for it to settle"];
return ["clear", "sessions table is bloated and no checkout is in progress"];
}
async function woo(path, options = {}) {
const res = await fetch(`${WOO_URL}/wp-json/wc/v3${path}`, {
...options,
headers: { "Content-Type": "application/json", Authorization: AUTH, ...(options.headers || {}) },
});
if (!res.ok) throw new Error(`Woo ${path} returned ${res.status}`);
return res.json();
}
async function getSystemStatus() {
return woo("/system_status");
}
async function recentLiveOrders(guardMinutes) {
const after = new Date(Date.now() - guardMinutes * 60000).toISOString();
return woo(`/orders?status=${LIVE_ORDER_STATUSES.join(",")}&after=${after}&per_page=50`);
}
async function getIntent(intentId) {
if (!intentId) return null;
try {
return await stripe.paymentIntents.retrieve(intentId);
} catch {
return null;
}
}
async function countOpenCheckouts(guardMinutes) {
const orders = await recentLiveOrders(guardMinutes);
let openCount = 0;
for (const order of orders) {
const intent = await getIntent(intentIdOf(order));
if (intent && OPEN_INTENT_STATUSES.has(intent.status)) openCount++;
}
return openCount;
}
async function clearSessions() {
await woo("/system_status/tools/clear_sessions", { method: "PUT" });
}
export async function run() {
const status = await getSystemStatus();
const sizeMb = sessionsTableSizeMb(status);
const openCheckouts = await countOpenCheckouts(CHECKOUT_GUARD_MINUTES);
const [action, reason] = decide(sizeMb, MAX_SESSIONS_MB, openCheckouts);
if (action === "skip") {
console.log(`Sessions table is ${sizeMb.toFixed(1)} MB, under the ${MAX_SESSIONS_MB} MB threshold. Nothing to do.`);
return;
}
if (action === "wait") {
console.warn(
`Sessions table is ${sizeMb.toFixed(1)} MB (over ${MAX_SESSIONS_MB} MB) but ${openCheckouts} ` +
`checkout(s) look in progress. ${reason}`
);
return;
}
console.log(
`Sessions table is ${sizeMb.toFixed(1)} MB (over ${MAX_SESSIONS_MB} MB) and no checkout is in progress. ` +
(DRY_RUN ? "Would clear it." : "Clearing it now.")
);
if (!DRY_RUN) {
await clearSessions();
console.log("Cleared wp_woocommerce_sessions.");
}
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
run().catch((err) => { console.error(err); process.exit(1); });
}
Add a test
The decision rule is the part most worth testing, because it decides whether a live shopper's cart gets wiped. Because we kept decide pure, the test needs no network, no Stripe account, and no real WooCommerce store. It just feeds in plain numbers and checks the action.
from clear_stale_sessions import decide, intent_id_of, sessions_table_size_mb
def test_skip_when_under_threshold():
assert decide(10.0, 50.0, 0)[0] == "skip"
def test_clear_when_over_threshold_and_no_open_checkout():
assert decide(120.0, 50.0, 0)[0] == "clear"
def test_wait_when_over_threshold_but_checkout_in_progress():
assert decide(120.0, 50.0, 2)[0] == "wait"
def test_skip_takes_priority_even_with_open_checkout():
# If the table isn't actually bloated yet, an in-progress checkout is irrelevant.
assert decide(5.0, 50.0, 3)[0] == "skip"
def test_intent_id_from_meta():
order = {"meta_data": [{"key": "_stripe_intent_id", "value": "pi_123"}], "transaction_id": ""}
assert intent_id_of(order) == "pi_123"
def test_sessions_table_size_mb_sums_data_and_index():
status = {"database": {"database_tables": {"other": {
"woocommerce_sessions": {"data": "4.10", "index": "0.15"}
}}}}
assert abs(sessions_table_size_mb(status) - 4.25) < 1e-9
import { test } from "node:test";
import assert from "node:assert/strict";
import { decide, intentIdOf, sessionsTableSizeMb } from "./clear-stale-sessions.js";
test("skip when under threshold", () => {
assert.equal(decide(10.0, 50.0, 0)[0], "skip");
});
test("clear when over threshold and no open checkout", () => {
assert.equal(decide(120.0, 50.0, 0)[0], "clear");
});
test("wait when over threshold but checkout in progress", () => {
assert.equal(decide(120.0, 50.0, 2)[0], "wait");
});
test("intentIdOf from meta", () => {
assert.equal(intentIdOf({ meta_data: [{ key: "_stripe_intent_id", value: "pi_123" }], transaction_id: "" }), "pi_123");
});
test("sessionsTableSizeMb sums data and index", () => {
const status = { database: { database_tables: { other: {
woocommerce_sessions: { data: "4.10", index: "0.15" },
} } } };
assert.ok(Math.abs(sessionsTableSizeMb(status) - 4.25) < 1e-9);
});
Case studies
The performance guide that broke its own cleanup
A store followed a well meaning performance guide that disabled WP-Cron and replaced it with a system cron entry, but the replacement only ever hit the homepage, not the admin-ajax endpoint that WooCommerce's scheduler needs. The sessions table quietly passed a hundred megabytes over a few months while nobody looked at system status.
Running this job in dry run first showed the exact size and confirmed no checkout was ever caught mid-run, since it only checked when nobody was buying. Once trusted, it ran hourly and kept the table under twenty megabytes from then on.
The flash sale that overwhelmed the cleanup query
During a busy sale weekend, so many sessions were created that the built in cleanup query started timing out before it could finish, so it kept failing without ever shrinking the table. By Monday the table was large enough to slow down every cart request storewide.
The checkout guard in this job waited out the last hour of real sale traffic, then cleared the backlog safely once the rush had genuinely ended, without an admin needing to guess when it was safe.
After this runs on a schedule, a stalled cleanup event is no longer a slow, creeping problem. The worst case becomes a short wait of one run cycle while a real checkout finishes. Keep it running even after you fix WP-Cron or Action Scheduler, since it is a cheap, read-mostly check that only ever acts when the table is actually oversized.
FAQ
Why does wp_woocommerce_sessions keep growing?
WooCommerce is supposed to delete expired session rows on a schedule, but that cleanup runs through WP-Cron and Action Scheduler. If WP-Cron is disabled, Action Scheduler is stuck, or the request that runs cleanup keeps timing out, expired rows are never removed and the table only ever grows.
Is it safe to clear the sessions table on a live store?
Yes, as long as you check first that no shopper is mid-checkout right now. Clearing sessions drops every active cart along with the expired ones, so a script should confirm there is no recent order with an open Stripe PaymentIntent before it runs, and default to dry run so you can see its plan first.
How big does the sessions table need to get before this is worth running?
There is no universal number, but many stores never need it under a few megabytes. Stores with real, unresolved cleanup problems have reported this single table reaching hundreds of megabytes or more, almost entirely expired rows. Set your own threshold and let the job tell you when it is crossed.
Related field notes
Citations
On the problem:
- WordPress.org support: the wp_woocommerce_sessions table growing very large despite a small amount of real data. wordpress.org/support/topic/woocommerce_session-table-is-too-big-despite-small-amount-of-data
- WordPress.org support: WooCommerce not clearing old sessions, with real reports of table size and row counts. wordpress.org/support/topic/woocommerce-not-clearing-old-sessions
- WooCommerce docs: understanding the system status report, including the database tables section. woocommerce.com/document/understanding-the-woocommerce-system-status-report
On the solution:
- WooCommerce developer docs: the system status tools endpoint, including the clear_sessions tool. developer.woocommerce.com/docs/apis/rest-api/v2/system-status-tools
- WooCommerce developer docs: the system status endpoint and its database table size fields. developer.woocommerce.com/docs/apis/rest-api/v3/system-status
- Stripe API: retrieve a PaymentIntent and read its current status. docs.stripe.com/api/payment_intents/retrieve
Stuck on a tricky one?
If you have a bug in WooCommerce, WooCommerce Subscriptions, or the WooCommerce Stripe gateway that you would rather hand off, this is the kind of work I do. Message me and we can work through it together.
Did this shrink your sessions table?
If this saved your store from a slow cart on every page, you can buy me a coffee. It is the best way to keep these field notes free and growing.
Buy me a coffee on Ko-fi