Update on Save and Update by Schedule
Update on Save reindexes during the save; Update by Schedule logs ids to a changelog table and lets cron catch up.
bin/magento indexer:set-mode {realtime|schedule} [indexer ...]Every indexer runs in one of two modes, and the mode is set per indexer rather than globally.
Update on Save
The CLI calls this mode realtime. Saving a product reindexes the affected rows before the save returns. The storefront is correct immediately, and the person who clicked Save waits for it. That cost is paid again for every row.
Update by Schedule
The CLI calls this schedule. Magento puts MySQL triggers on the tables the indexer depends on. Each write appends the changed entity id to a changelog table named after the view id plus _cl, so catalog_product_price_cl, with columns version_id and entity_id. The machinery is called mview, short for materialised view. The cron group index runs the job indexer_update_all_views every minute; it reads the ids newer than the version_id stored in the mview_state table and hands them to the indexer, then records the new version.
Why an import must not run on save
In Update on Save, importing 20,000 products runs the price, category and stock indexers 20,000 times. In Update by Schedule the triggers write changelog rows and cron rebuilds them in batches. Switch to schedule before the import.
Switching mode is also what creates or drops the triggers, and it takes effect at once with no cache command. In 2.4.8 and later customer_grid supports both modes; earlier versions supported Update on Save only.
Example
<!-- vendor/magento/module-indexer/etc/crontab.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd">
<group id="index">
<job name="indexer_reindex_all_invalid"
instance="Magento\Indexer\Cron\ReindexAllInvalid" method="execute">
<schedule>* * * * *</schedule>
</job>
<job name="indexer_update_all_views"
instance="Magento\Indexer\Cron\UpdateMview" method="execute">
<schedule>* * * * *</schedule>
</job>
<job name="indexer_clean_all_changelogs"
instance="Magento\Indexer\Cron\ClearChangelog" method="execute">
<schedule>*/5 * * * *</schedule>
</job>
</group>
</config>When to use it
- Preparing a store for a nightly product feed of 50,000 SKUs.
- Working out why a price saved in the admin appears a minute later instead of at once.
- Deciding which indexers may stay on save on a small store whose cron is unreliable.
- Diagnosing a changelog table that has grown to millions of rows.
More examples
Switching a store to Update by Schedule
With no indexer argument, set-mode applies to every indexer, which is the usual setting for a production store. show-mode is the quick confirmation; indexer:status is the fuller one, and only shows the Schedule Status and Schedule Updated columns for indexers that are in schedule mode. A backlog that never falls to 0 means cron is not running.
$ bin/magento indexer:set-mode schedule
Index mode for Indexer Design Config Grid was changed from 'Update on Save' to 'Update by Schedule'
Index mode for Indexer Customer Grid was changed from 'Update on Save' to 'Update by Schedule'
Index mode for Indexer Category Products was changed from 'Update on Save' to 'Update by Schedule'
...
$ bin/magento indexer:show-mode catalog_product_price
Product Price: Update by Schedule
$ bin/magento indexer:status catalog_product_price
+-----------------------+---------------+--------+-----------+---------------------+---------------------+
| ID | Title | Status | Update On | Schedule Status | Schedule Updated |
+-----------------------+---------------+--------+-----------+---------------------+---------------------+
| catalog_product_price | Product Price | Ready | Schedule | idle (0 in backlog) | 2026-10-05 08:14:02 |
+-----------------------+---------------+--------+-----------+---------------------+---------------------+Looking at the changelog by hand
When a saved change does not reach the storefront, these three queries settle who is at fault. A changelog row with a version_id above the one in mview_state means the change was recorded and cron has not processed it yet. No new changelog row at all means the triggers are missing or you wrote to the database in a way that bypassed them. SHOW TRIGGERS confirms which it is.
-- the triggers write one row per changed entity
SELECT * FROM catalog_product_price_cl ORDER BY version_id DESC LIMIT 5;
-- version_id | entity_id
-- 90412 | 1455
-- 90411 | 1454
-- 90410 | 1453
-- mview_state records how far the indexer has consumed the changelog
SELECT view_id, mode, status, version_id, updated
FROM mview_state WHERE view_id = 'catalog_product_price';
-- catalog_product_price | enabled | idle | 90412 | 2026-10-05 08:14:02
-- the triggers themselves, created when the mode was set to schedule
SHOW TRIGGERS LIKE 'catalog_product_entity';A bulk import, in the right order
Step 3 is the one people miss on a development machine, where cron is often not installed at all. cron:run --group=index runs the index group once in the foreground, which processes the changelog immediately. On a real server the system crontab does this every minute and step 3 is unnecessary.
# 1. make sure nothing reindexes inside each row's save
bin/magento indexer:set-mode schedule catalog_product_price \
catalog_category_product catalog_product_category \
cataloginventory_stock catalogsearch_fulltext
# 2. run the import (admin grid, or your own CLI command)
bin/magento vendor:feed:import /var/feeds/products.csv
# 3. let mview catch up now rather than waiting for the cron tick
bin/magento cron:run --group=index
# 4. check the backlog is draining
bin/magento indexer:status
Discussion