Magento does not clean up after itself
A Magento database grows every day it runs. Some of that growth is real business data: orders, customers, products. A large share of it is log, report, session, and quote data that Magento writes constantly and prunes almost never.
This article shows which tables grow without limit, how to measure what is actually taking up space, and how to clean them without breaking the store.
We will name the specific offenders, write the SQL to rank them, separate one-time size from ongoing growth rate, and set up a cleanup routine that keeps the problem from coming back.
Why a 40GB database is rarely 40GB of business data
When we open an audit on a store that feels slow and expensive to host, the database is often several times larger than the order history justifies. A store with 80,000 lifetime orders does not need a 60GB database.
The gap is almost always in a handful of tables that Magento treats as scratch space. They record activity, then keep every row forever because nothing is configured to remove them.
log:clean command the way Magento 1 did. Most of these tables are never pruned unless you configure a cron to do it or delete the rows yourself.Database bloat is not the same as log files on disk
One clarification before the tables, because the two get confused constantly. The bloat this article covers lives in MySQL or MariaDB, in the tables Magento writes to.
That is separate from the log files under var/log, such as system.log, debug.log, and exception.log. Those sit on the filesystem and fill the disk in their own way, especially when debug logging is left on in production.
Both are real sources of disk pressure and both belong in an audit. They are cleaned differently, so keep them straight. This article is about the database; the files on disk are their own routine.
The families of tables that grow without limit
The tables that bloat fall into a few groups, and knowing the group tells you why the table grows and how safe it is to clean:
- Report and behavior tables, like recently-viewed and compared products and report events. Written on customer activity, rarely read, and almost never pruned automatically.
- Quote and cart tables, where
quoteand its children hold every cart ever started, including bot and abandoned carts, until a cron deletes the expired ones. - Session and operational tables, meaning database-backed sessions, async bulk operations, and admin notifications. These grow with traffic and background work, not with sales.
- Scheduling and import tables, such as
cron_scheduleand legacy dataflow tables, which fill up when cron is misconfigured or large imports run repeatedly.
Those groups cover the large majority of what we find. The exact worst offender varies by store, which is why you measure before you touch anything.
Report and log tables: the most common offenders
The Reports module records customer behavior into a set of tables that grow with every session. On a busy store these are frequently the single largest source of bloat.
report_event and the recently-viewed tables
Watch report_event, report_viewed_product_index, report_compared_product_index, and catalog_product_frontend_action. Every product view, comparison, and "recently viewed" interaction writes rows here.
None of these has automatic cleanup in a default install. A store that has run for three years can hold tens of millions of rows in report_event alone.
Whether you can just truncate them
These tables are safe to truncate on most stores, because they hold derived behavior data, not orders or customers. The cost is losing the "recently viewed" and "compared products" storefront blocks and the behavioral reports in the admin.
If nobody uses those reports, and most stores do not, truncating them is the fastest single reduction you will get. Confirm the store is not surfacing recently-viewed products on the frontend before you do it.
How to stop report_event from refilling
Truncating report_event without stopping the writes buys you a few weeks. The table is populated by observers in the Magento_Reports module on product views, cart adds, wishlist activity, and comparisons.
If the store does not use behavioral reports or the recently-viewed and compared-products blocks, disable the module at the source with bin/magento module:disable Magento_Reports. That stops the writes entirely and removes the admin Reports section those tables feed.
That is a real tradeoff, not a free win. Confirm nobody on the merchandising side relies on the product-views or bestsellers reports before you turn it off.
On many stores the largest tables are behavior and session data, not orders.
Quote and cart tables: growth from carts that never became orders
The quote table holds one row for every cart a visitor starts. Its children, quote_item, quote_address, quote_payment, and quote_item_option, multiply that by everything added to those carts.
Most of those carts never become orders. Bots, price scrapers, and abandoned shopping all create quote rows that sit forever unless cleanup runs.
Magento does have a mechanism for this. The clean_expired_quotes cron job deletes quotes older than the lifetime set in checkout/cart/delete_quote_after, expressed in days.
When the quote tables are huge, one of two things is true. Either cron is not running the cleanup job, or the quote lifetime is set far longer than the business needs. Both are worth checking before you delete anything manually.
Session and operational tables
If app/etc/env.php sets the session save handler to db, sessions live in the session table. That table then grows with every visitor and relies on garbage collection to stay bounded.
Database session storage is usually the wrong choice on a store of any size. Redis or Valkey handles sessions better and keeps them out of the database entirely.
Two more operational tables show up in audits. magento_operation and related bulk tables grow with async and message-queue work, and adminnotification_inbox collects every Adobe notification the store has ever received.
cron_schedule: a table that should always stay small
The cron_schedule table records every scheduled job Magento plans and runs. On a healthy store it stays small because Magento cleans its own history.
That cleanup is configured under Stores, Configuration, Advanced, System, Cron. Each cron group has a history lifetime for successful and failed jobs, and a schedule for how often the history is pruned.
When cron_schedule holds hundreds of thousands of rows, one of three things is wrong. Cron is not running its own maintenance job, a job is being scheduled far more often than it needs to be, or the history lifetime was set to something unreasonable.
The fix is the configuration and cron health, not a truncate. Empty the table without fixing the cause and it refills within days.
How to measure what is actually big
Do not guess which table is the problem. Rank them by size directly against information_schema, which every MySQL and MariaDB install exposes.
SELECT table_name,
ROUND((data_length + index_length) / 1024 / 1024, 1) AS size_mb,
table_rows
FROM information_schema.tables
WHERE table_schema = DATABASE()
ORDER BY (data_length + index_length) DESC
LIMIT 25;
Run that against the Magento database and read the top ten. The offenders from the sections above will usually be sitting right at the top, well ahead of sales_order and catalog_product_entity.
Note that table_rows from information_schema is an estimate for InnoDB, not an exact count. It is accurate enough for ranking, which is all you need at this stage.
Reading the two numbers: data and index
The sizing query adds data_length and index_length together. Split them apart and you learn something the total hides.
When a table's index_length rivals or exceeds its data_length, the indexes are a large part of the problem. That is common on report_event and the quote tables, and it also shows up where an extension added indexes that were never removed.
For the bloat tables you plan to truncate, this does not change the plan, since truncation drops the data and the indexes together. It matters for the tables you keep, like sales_order and catalog tables, where an oversized index is a separate finding worth its own review.
Size is not the same as growth rate
A big table is a symptom. A fast-growing table is the actual problem, because it tells you where the store is heading, not just where it has been.
To measure growth, snapshot the sizing query into a small table or a dated file, then run it again a week later and compare. The delta per table per day is what matters.
A report_event table adding two million rows a week is a different priority than one that is large but static. The first will be back to its old size a month after you truncate it unless you also stop the growth.
You do not need a monitoring tool for this. A tiny history table and the same sizing query, run on a schedule, is enough to see the trend:
CREATE TABLE _size_history (
captured_at DATE,
table_name VARCHAR(255),
size_mb DECIMAL(10,1)
);
INSERT INTO _size_history
SELECT CURDATE(), table_name,
ROUND((data_length + index_length) / 1024 / 1024, 1)
FROM information_schema.tables
WHERE table_schema = DATABASE();
Run that insert once now and again in a week. The difference per table, divided by the days between captures, is your growth rate in megabytes per day.
Measuring growth over time separates a one-time cleanup from a recurring one.
Cleaning safely without breaking anything
The safe move is to match the method to the table. Behavior tables can be truncated. Quote and operational tables should be cleaned through their intended mechanism, not a blind delete.
- Report and recently-viewed tables:
TRUNCATEis acceptable once you confirm the related storefront blocks and reports are not in use. - Quote tables: fix the
clean_expired_quotescron and thecheckout/cart/delete_quote_afterlifetime, then let cleanup run, rather than truncating live carts. cron_schedule: correct the cron configuration and history lifetime instead of truncating, or you will just refill it.- Sessions: move session storage to Redis, which empties the table by making it unused.
quote on a live store to save space. You will delete every active shopping cart, including customers mid-checkout. Clean expired quotes through cron and lifetime configuration instead.Reclaiming disk after a large delete
Deleting rows from an InnoDB table does not automatically return the space to the operating system. The table keeps its allocated size and reuses the freed pages for new rows.
To actually shrink the file on disk you run OPTIMIZE TABLE, which rebuilds the table. On a store with innodb_file_per_table enabled, and it should be, that releases the space back to the filesystem.
Rebuilding a large table locks it and takes time, so run it in a maintenance window. If you skip this step, the sizing query will still show the old size even though the rows are gone.
Before you count on this, confirm the server actually stores tables in their own files. Check SHOW VARIABLES LIKE 'innodb_file_per_table'; and expect ON.
When it is off, InnoDB keeps everything in one shared ibdata1 file that never shrinks, and OPTIMIZE TABLE will not return space to the disk. Fixing that is a larger operation, so flag it as its own finding rather than a quick step in this cleanup.
A repeatable cleanup routine
A one-time cleanup buys a few months. A routine keeps the database bounded for good, which is the real goal.
The routine has three moving parts. Configure the built-in cleanup that Magento already offers, disable data collection you do not use, and monitor growth so a new offender does not appear unnoticed.
- Make sure cron is healthy so
clean_expired_quotesand cron history cleanup actually run. - Set a sane quote lifetime for your business, often 14 to 30 days rather than the default.
- If you do not use behavioral reports, turn off report event collection so
report_eventstops filling. - Keep sessions in Redis or Valkey, not the database.
- Re-run the sizing query on a schedule and watch the growth rate, not just the size.
None of this is exotic. It is configuration and a recurring check, which is exactly why it gets skipped for years until the database becomes a hosting and backup problem.
The tables you must not touch
The reason to measure first is that some large tables are business data, not bloat. Deleting from them is not cleanup, it is data loss.
Leave these alone no matter how big they get:
sales_order,sales_invoice,sales_shipment, and their line-item children. This is your order history.customer_entityand its attribute tables. These are your customers.- The
catalog_product_entityandcatalog_category_entityfamilies. These are your catalog. - The
*_gridtables such assales_order_grid. They are rebuildable, but they are read constantly by the admin and should be reindexed, not truncated blindly.
If one of these is genuinely oversized for the order or customer count, that is an archiving and indexing conversation, handled carefully, not a truncate. It is a different problem from the scratch-space tables this article is about.
When bloat becomes customer-facing
Table bloat is easy to ignore because it is invisible until it is not. The store keeps working while the database quietly gets slower and more expensive to run.
The point where it turns into an incident is usually one of these: backups that no longer finish inside their window, replication that lags behind the primary, disk that fills and takes the site down, or queries against a bloated table that slow the storefront.
By the time it is customer-facing, the fix is the same work described here, done under pressure instead of on a schedule.
What to do with this
Run the sizing query on your store this week. If the largest tables are behavior, session, or quote data rather than orders and products, you have measurable bloat and a clear path to reduce it.
Knowing which tables are growing, how fast, and which are safe to clean turns an ongoing hosting cost into a solved problem. Ranking that against everything else affecting performance is where a structured platform review earns its keep.