When the Storefront Is Stale
A change that will not appear is an index, a cache type, or the full page cache; check those three in that order.
bin/magento indexer:status && bin/magento cache:statusYou saved a change and the storefront still shows the old value. Three layers sit between the data and the page. Checking them in order is faster than guessing, because each one can hide the one below it.
1. Is the index fresh?
Run bin/magento indexer:status. Reindex required answers the question. In Update by Schedule, a Ready status with a growing backlog means cron is not running: try bin/magento cron:run --group=index and look at the cron_schedule table. If you changed rows with raw SQL, no trigger fired and no changelog row exists, so the index will never notice. Reindex that indexer explicitly.
2. Is a cache type invalidated?
bin/magento cache:status lists the types and whether each is enabled. Admin > System > Cache Management flags the invalidated ones. A change to core_config_data needs bin/magento cache:clean config; block output lives in block_html. bin/magento cache:flush clears every type and is the blunt answer.
3. Is the page itself cached?
The full_page type stores whole HTML responses, so a correct index and a correct block still serve old markup. Clear it with bin/magento cache:clean full_page. A reindex normally handles this for you: indexers register the entities they touched on Magento\Framework\Indexer\CacheContext, and the pages tagged with those entities are invalidated. Code that writes data without registering anything skips that step, which is the usual reason a custom import leaves stale pages behind.
Example
<?php
// app/code/Vendor/Feed/Model/PriceWriter.php
declare(strict_types=1);
namespace Vendor\Feed\Model;
use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexerProcessor;
use Magento\Framework\App\ResourceConnection;
class PriceWriter
{
public function __construct(
private readonly ResourceConnection $resource,
private readonly PriceIndexerProcessor $priceIndexerProcessor
) {
}
/**
* @param array<int, float> $pricesByProductId
*/
public function write(array $pricesByProductId, int $attributeId, int $storeId): void
{
$connection = $this->resource->getConnection();
$table = $this->resource->getTableName('catalog_product_entity_decimal');
foreach ($pricesByProductId as $productId => $price) {
$connection->update(
$table,
['value' => $price],
[
'attribute_id = ?' => $attributeId,
'store_id = ?' => $storeId,
'entity_id = ?' => (int) $productId,
]
);
}
// A raw UPDATE fires no model events, so say so explicitly.
$this->priceIndexerProcessor->markIndexerAsInvalid();
}
}
// After this runs: indexer:status shows catalog_product_price as
// "Reindex required", and the cron job indexer_reindex_all_invalid
// rebuilds it on its next tick.When to use it
- A price edited in the admin that does not change on the category page.
- A custom import that updates the database but leaves the storefront showing old values.
- A template change that appears in one environment and not in production.
- A staging site where every change needs two reloads to show up.
More examples
The three checks, in order
Run them one at a time and reload the page between each, so you learn which layer was actually holding the old value. Reaching for cache:flush first does usually make the problem go away, and teaches you nothing about where it came from, which is why the same bug comes back next week.
# 1. index
$ bin/magento indexer:status catalog_product_price
| catalog_product_price | Product Price | Reindex required | Schedule | idle (0 in backlog) |
$ bin/magento indexer:reindex catalog_product_price
Product Price index has been rebuilt successfully in 00:00:51
# 2. caches
$ bin/magento cache:status
config: 1
layout: 1
block_html: 1
full_page: 1
# 3. the cached page
$ bin/magento cache:clean full_page
Cleaned cache types:
full_pageProving cron is the problem
Rows with status missed or no rows at all mean the system crontab is not calling bin/magento cron:run. A changelog maximum well above the mview_state version means cron is behind rather than broken. Equal values with the storefront still stale means mview did its job and the problem is further down, in a cache.
-- is the index group running at all?
SELECT job_code, status, scheduled_at, executed_at, messages
FROM cron_schedule
WHERE job_code = 'indexer_update_all_views'
ORDER BY scheduled_at DESC LIMIT 5;
-- indexer_update_all_views | success | 2026-10-05 08:14:00 | 2026-10-05 08:14:01 | NULL
-- indexer_update_all_views | missed | 2026-10-05 08:13:00 | NULL | NULL
-- did the change reach the changelog?
SELECT MAX(version_id) FROM catalog_product_price_cl;
SELECT version_id FROM mview_state WHERE view_id = 'catalog_product_price';cache:clean and cache:flush are not the same
On a shared Redis or Memcached backend, cache:flush empties everything in it rather than just Magento's keys, which can take out a neighbouring application. cache:clean is the safer habit and is almost always enough. Keep cache:flush for the case where the configuration cache itself is wrong, since a bad config cache can stop cache:clean resolving the right types.
# cache:clean removes only the entries Magento tagged as its own
bin/magento cache:clean config
# cache:flush empties the whole storage backend, including entries
# written by other applications sharing that Redis instance
bin/magento cache:flush
# which types exist, and which are switched off
bin/magento cache:status
Discussion