Magento Indexers: Stuck, Invalid, or Silently Wrong

Magento Indexers: Stuck, Invalid, or Silently Wrong

The report is almost never "our indexers are broken." It's "this product isn't showing up in the category" or "the price on the site doesn't match the admin" or "search finds it but the category doesn't." Somebody re-saves the product, it appears, and everyone moves on without asking why it took a manual save.

Indexing is where a lot of Magento's quiet failures live, because when it goes wrong the store keeps serving pages. It just serves the wrong ones.

What the indexers are actually for

Magento stores product data in EAV — attributes spread across separate tables by data type. Reading a category page directly out of that structure would mean dozens of joins per product, so instead Magento precomputes flat tables optimized for reading, and the indexers are what keep those tables in step with the source data.

There are around a dozen. The ones that cause visible problems:

  • Category Products and Product Categories — which products belong in which category. This is the one behind "the product isn't in the category."
  • Product Price — computed prices including tier and special pricing. Behind "the price is wrong."
  • Stock — salable quantity.
  • Catalog Rule Product — which catalog price rules apply to what.
  • Catalog Search — what gets pushed into OpenSearch.

When one of these is stale, the storefront serves the last good version of it. No error, no warning, no 500. The page renders perfectly and the contents are out of date.

Two modes, and the one that matters

Each indexer runs in one of two modes.

Update on Save reindexes the affected rows during the save itself. Simple, immediate, and it makes every admin save slower — sometimes dramatically. A mass import with this mode on can take hours, because each row triggers indexing work.

Update by Schedule writes the changed entity IDs into a changelog table and lets cron process them in batches. Saves stay fast, indexing happens in the background.

For any store with a real catalog, Update by Schedule is the right answer. Check what you have:

bin/magento indexer:show-mode

The important consequence is that Update by Schedule depends entirely on cron. Cron stops, indexing stops, and nothing announces it. This is the single most common version of the problem, and it's why the cron check below comes before anything else.

The mechanism, briefly

When an indexer is switched to Update by Schedule, Magento creates database triggers on the source tables and a changelog table named <indexer>_cl. A change to a product writes its ID into catalog_category_product_cl. Cron reads new rows since the last processed version, reindexes those IDs, and advances the pointer in mview_state.

Three things can go wrong with that, and they produce different symptoms:

  • Cron isn't running. Rows accumulate in the changelog and nothing consumes them. The index is stale and getting staler.
  • The pointer is wrong or the triggers are missing. Changes aren't recorded, or are recorded and never read. The changelog grows without limit.
  • mview_state is stuck in working. A previous run died partway. Cron sees it as still running and never starts another.

Working out which one you have

Start here, in this order.

1. What do the indexers say?

bin/magento indexer:status

Ready is fine. Reindex required means invalid and waiting. Processing on every run, or for hours, means something is stuck.

2. Is cron actually running?

SELECT job_code, status, executed_at, finished_at
FROM cron_schedule
WHERE status = 'success'
ORDER BY finished_at DESC
LIMIT 10;

If the newest successful row is hours old, that's the answer and everything below is a symptom of it. Also check the table hasn't filled with pending rows that never ran, and that it isn't enormous — an uncleaned cron_schedule with millions of rows becomes its own problem.

3. How big are the changelog tables?

SELECT table_name, table_rows
FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name LIKE '%_cl'
ORDER BY table_rows DESC;

A few thousand rows is normal on a busy store. Millions means they aren't being consumed. A changelog table that has grown into the gigabytes is a common reason a reindex starts and never finishes: each run tries to process an enormous backlog and times out before it can advance the pointer, so the backlog is the same size next time.

4. Are the triggers there?

SHOW TRIGGERS LIKE 'catalog_product_entity';

Missing triggers on an indexer set to Update by Schedule means changes are never recorded at all. This happens after a database restore from a dump that didn't include triggers, which is a common enough accident to be worth checking specifically after any migration.

5. Is anything stuck in working?

SELECT * FROM mview_state;

A row in working whose updated timestamp is old is a dead run blocking every subsequent one.

Fixing it

The fixes depend on the cause, which is the reason for diagnosing first. Reindexing everything is the reflex and it usually just hides the problem for a day.

If cron isn't running, fix that. Nothing else matters until it is, and once it is the backlog may clear on its own.

If a changelog has grown unmanageable, the usual approach is to reset the affected indexer and let it rebuild — which truncates the changelog and does a full reindex. Do it in a maintenance window on a large catalog, because a full reindex on a big store is not quick.

If mview_state is stuck, the stale working row has to be cleared before cron will start a new run.

If triggers are missing, switching the indexer to Update on Save and back to Update by Schedule recreates them.

In every case: take a database backup first, and do it on staging first if there's a staging environment. These operations touch the tables the storefront reads from.

The part that should bother you

None of this raises an alarm.

A stale index doesn't throw an error, doesn't appear in the admin as a warning anyone will see, and doesn't break a page. Products quietly stop appearing in a category. A price quietly stops matching. The store carries on taking orders for everything that is still visible, and nobody finds out until a customer asks about a product they can't find, or somebody notices the numbers.

Which means the useful thing isn't knowing how to fix this — it's having something that checks. Indexer status and the age of the newest successful cron row are two queries. Anything that runs them on a schedule and complains is worth more than the time it takes to set up, and most stores have nothing of the kind.

That check is part of what a retainer buys, and it's one of the first things we look at in a free store health check — because a stale indexer is invisible from outside the store, and it's costing money the whole time it's wrong.

If this is happening on your store now, tell us what you're seeing and we'll tell you which of the five checks above to run first.

Frequently asked questions

Why are my products not showing in a Magento category?

Most often the category products index is stale or invalid, so the storefront is serving an old view of which products belong where. Check the indexer status first with bin/magento indexer:status before looking at the category's own settings — if the index is invalid, the category configuration is probably fine and simply isn't being applied.

Should Magento indexers be on Update on Save or Update by Schedule?

Update by Schedule, for any store with more than a trivial catalog. Update on Save reindexes during the save itself, which makes admin saves slow and mass imports extremely slow. Update by Schedule records the change and lets cron process it in batches. The trade is that it depends on cron actually running.

What are Magento changelog tables and why do they grow?

Each indexer running on Update by Schedule has a cl table that records which entity IDs changed. Cron reads it, reindexes those rows, and advances a version pointer. If the subscriptions or the pointer get out of step the rows stop being consumed and the table grows without limit, which is a common cause of a reindex that never finishes.

How do I tell if Magento cron is actually running?

Query the cron_schedule table for recent rows with a status of success. If the newest successful row is hours old, cron is not running properly, and every indexer on Update by Schedule has stopped updating along with it.

Work with Emyrix

Want to know how your own store is doing?

Send us the URL and a Magento engineer will look at it — version and patch status, page speed, caching, indexing, checkout — then email you what we found. Free, and there is no obligation attached to it.