037 —Magento
Magento 2 indexer vastgelopen: herstel zonder orderverlies
Een vastgelopen Magento 2 indexer geeft foute prijzen, ontbrekende producten en spookvoorraad. Dit is de herstelvolgorde die lopende orders beschermt.
Vrijdag, 19:14. Een Nederlandse ops engineer pingt het on-call kanaal: orders in een Magento 2.4.6 shop gaan door met prijzen van vorige week. De promobanner toont de juiste korting, de productpagina toont de juiste korting, maar de cart en de bevestigingsmail tonen de oude €89,95 in plaats van de €69,95 uit de campagne. Klantenservice zit al aan de derde "wat wordt er nu eigenlijk afgeschreven"-mail van het uur.
De oorzaak, zodra je inlogt en bin/magento indexer:status draait, is duidelijk: catalog_product_price staat al negen uur op Processing. Welke cron-run de reindex ook deed, hij is nooit teruggekomen.
Dit is de failure mode die elke Magento 2-operator vroeg of laat erft. Het herstel zelf is mechanisch, maar de volgorde waarin je het doet telt, en hier een stap overslaan is hoe je op zondagochtend met dataverlies zit in plaats van een vrijdagavond-incidentrapport. Hieronder de sequentie die wij draaien als de Magento 2 indexer vastloopt.
Controleer wat er echt vastzit
Voordat je iets aanraakt: haal de feitelijke status op. SSH in op de productie-node, switch naar de Magento file-user (vaak www-data of een deploy-user), en draai:
php bin/magento indexer:statusJe ziet dan iets als:
Customer Grid Ready
Category Products Ready
Product Categories Ready
Product Price Processing
Product EAV Ready
Stock Ready
Catalog Rule Product Reindex required
Catalog Product Rule Ready
Catalog Search ProcessingTwee states zijn hier belangrijk. Processing (in oudere patches weergegeven als working) betekent dat een worker de indexer heeft geclaimd en hem nooit heeft vrijgegeven. Reindex required betekent dat de mview-queue wijzigingen klaar heeft staan en niemand ze consumeert. Het eerste is een lock-probleem; het tweede een backlog-probleem.
Cross-check tegen de database. Open de MySQL editor die je tegen Magento gebruikt (of mysql vanuit de shell) en draai:
SELECT indexer_id, status, updated
FROM indexer_state
WHERE status <> 'valid'
ORDER BY updated;De kolom updated vertelt je of dit een lopende job is die meer tijd nodig heeft of een lijk uit eerder die week. Alles ouder dan de langste legitieme reindex die je op deze catalogus hebt gezien (meestal 20 tot 40 minuten voor een middelgrote shop) is een lijk.
Check daarna de mview-queue:
SELECT view_id, mode, status, updated
FROM mview_state
ORDER BY updated;Staat mode op enabled en status op working, dan denkt de materialised-view worker dat hij nog steeds de changelog aan het consumeren is. Is hij in werkelijkheid gecrasht, dan blijven de changelog-tabellen (catalog_product_price_cl, catalogsearch_fulltext_cl en hun broertjes) groeien terwijl niemand ze leegtrekt.
Vind het vastlopende proces voordat je de lock kapotmaakt
Onderdruk de neiging om de indexer meteen te resetten. Is er nog een echt PHP-proces bezig met het werk, dan zit je na het resetten van de state-rij met twee processen die in dezelfde index-tabellen schrijven. Zo corrumpeer je catalog_product_index_price.
Op de applicatieserver:
ps -ef | grep -E 'indexer:reindex|cron:run' | grep -v grepEn op de database-server: check op langlopende queries die rijen vasthouden:
SELECT id, user, host, db, time, state, LEFT(info, 120) AS query
FROM information_schema.processlist
WHERE time > 300
ORDER BY time DESC;Zie je een Magento-proces dat al uren leeft maar daadwerkelijk voortgang maakt (CPU-gebruik, tabellen groeien, locks worden genomen en vrijgegeven), laat het staan. Is het PHP-proces verdwenen maar staat de rij nog op working, dan heb je een verweesde state en kun je beginnen met herstellen.
Reset de indexer-state veilig
Zodra de orphan bevestigd is, reset alleen de specifieke indexers die vastzitten, niet de hele set. Alles resetten forceert een volledige rebuild die op een shop met 50.000 SKU's een uur kan duren, met een storefront die in de tussentijd stale data serveert.
php bin/magento indexer:reset catalog_product_price catalogsearch_fulltextDat zet die rijen in indexer_state terug op invalid zodat de volgende reindex-run ze oppakt. Bevestig:
SELECT indexer_id, status FROM indexer_state
WHERE indexer_id IN ('catalog_product_price','catalogsearch_fulltext');Je wil invalid zien staan, niet working.
Staat mview_state nog steeds op working, ruim dat dan ook op:
UPDATE mview_state
SET status = 'idle'
WHERE view_id IN ('catalog_product_price','catalogsearch_fulltext')
AND status = 'working';Maak een back-up van deze twee rijen voordat je de UPDATE doet. Ze zijn klein, en je wil ze misschien later vergelijken als je de post-mortem schrijft.
Rebuild in de juiste volgorde
De indexers van Magento hebben impliciete afhankelijkheden. catalogsearch_fulltext leest uit de product-, categorie- en prijsindex. catalog_product_price leest uit stock- en rule-data. Fulltext eerder rebuilden dan price geeft je het komende half uur een zoekindex die naar de verkeerde prijzen wijst.
Draai ze in deze volgorde:
php bin/magento indexer:reindex cataloginventory_stock
php bin/magento indexer:reindex catalogrule_rule
php bin/magento indexer:reindex catalog_product_price
php bin/magento indexer:reindex catalog_category_product
php bin/magento indexer:reindex catalog_product_category
php bin/magento indexer:reindex catalogsearch_fulltextVoor een grote catalogus: draai ze in aparte terminal-sessies of met nohup, zodat een afgebroken SSH-verbinding de reindex niet halverwege onderuit haalt. Let op het geheugen: de prijs-reindex op een multi-website shop kan pieken boven de 2 GB. Sterft PHP met een out-of-memory error, dan ben je terug bij af, dit keer met half opgebouwde tabellen.
Adobe documenteert de hele indexer-set in de Adobe Commerce indexer reference als je de namen tegen jouw versie wil verifiëren.
Controleer of orders niet vervuild zijn
Dit is de stap die elke recovery-gids overslaat. Terwijl de index vastzat serveerde de storefront stale prijzen, en de orders die in dat window geplaatst zijn staan in je database met wat de cart heeft uitgerekend. Sommige kloppen (de prijs is in die periode niet veranderd). Sommige staan op de oude prijs en de klant heeft het oude bedrag betaald. Sommige staan op de oude prijs maar de gateway heeft het nieuwe bedrag afgeschreven omdat de korting anders is toegepast. Je moet weten welke welke is.
Vind het verdachte window:
SELECT o.entity_id, o.increment_id, o.created_at,
i.sku, i.qty_ordered, i.price, i.original_price
FROM sales_order o
JOIN sales_order_item i ON i.order_id = o.entity_id
WHERE o.created_at BETWEEN '2026-05-22 10:00:00' AND '2026-05-22 19:14:00'
AND i.sku IN (
SELECT sku FROM catalog_product_entity
WHERE entity_id IN (/* SKUs that were on promo */)
)
ORDER BY o.created_at;Vergelijk price met de campagneprijs per regel. Alles wat op het verkeerde bedrag is doorgekomen, is een klantenservice-ticket dat staat te wachten. Restitueer het verschil pro-actief. Drie pre-emptieve refundmails van €20 op vrijdagavond kosten minder dan vijftig boze tweets op zaterdagochtend.
Voorkom dat het weer gebeurt
Een indexer loopt vast om één van vier redenen, en per oorzaak verschilt de fix.
De cron-user is door OOM gekilled
Check dmesg of /var/log/syslog op de out-of-memory killer van de kernel. De reindex van Magento op een grote catalogus heeft regelmatig 1,5 tot 2 GB nodig. Heeft je container of VM PHP gecapped op 512 MB, dan sterft hij midden in de job en blijft de rij op working staan. Verhoog memory_limit in de PHP CLI-configuratie, niet die van FPM, want cron gebruikt de CLI-binary.
De database-verbinding is timed-out
Kijk in var/log/cron.log naar MySQL server has gone away. Langlopende reindexes kunnen wait_timeout overleven. Verhoog hem aan MySQL-kant voor de deploy-user, of verklein de indexer-batchsize in app/etc/env.php:
'indexer' => [
'batch_size' => [
'catalogsearch_fulltext' => [
'partial_reindex' => 100,
'mysql_get' => 500,
'elastic_save' => 500,
],
],
],Twee reindexes raceten
Vuurden een deploy-script en een cronjob beide indexer:reindex tegelijk af, dan blokkeert de tweede op de locks van de eerste en time-out hij uiteindelijk met een verwarde state als gevolg. Zet reindex in je deploy-pipeline achter één enkele lock.
De mview-changelog groeide sneller dan hij kon leegtrekken
Kijk naar de backlog:
SELECT COUNT(*) FROM catalog_product_price_cl;
SELECT COUNT(*) FROM catalogsearch_fulltext_cl;Staan die getallen in de miljoenen, dan heeft een import of bulk-update de mview-consumer overspoeld. Zet de betreffende indexers op schedule-mode en verwerk ze in aparte cron groups, zodat een prijsupdate voor 200.000 SKU's de zoekindex niet uithongert.
Houd een papieren spoor bij
Elke fix aan een vastgelopen indexer is óók een wijziging aan de database. De UPDATE mview_state, de row-backups, de tweak aan env.php: schrijf elk daarvan in je incident note terwijl je het doet. Twee weken later, als een soortgelijke vastloper op de staging-kloon optreedt, wil de persoon die dan oncall is precies weten wat je hebt gedaan en in welke volgorde. Een diff van het configbestand is meer waard dan een Slack-bericht.
Toen wij Pier bouwden voor dit soort werk aan legacy sites, was het stuk van de workflow dat het meeste denkwerk vroeg precies dit. Elke SQL-statement die wij tegen de database van een klant draaien wordt vastgelegd, met one-click revert en een complete version history per bestand. De Magento-operators die met ons getest hebben vroegen eerst om die audit log, voor ook maar één van de chat-features.
Het kleinste wat je vandaag kunt doen: draai bin/magento indexer:status op elke Magento 2-shop die je beheert, schiet een screenshot en pin het in je runbook naast de herstelcommando's hierboven. Als het telefoontje om 23:41 op een vrijdagavond komt, zit het verschil tussen een fix van 20 minuten en een outage van 4 uur erin of je de status kunt lezen zonder na te denken.
— Vragen —
Waarom is indexer:reset alleen niet genoeg om de indexer los te trekken?
Reset zet alleen de state-rij terug op invalid. Houdt er nog een PHP-worker de lock vast, of staat ook mview_state op working, dan moet je het orphan-proces apart killen en mview_state los bijwerken.
Is het veilig om de *_cl changelog-tabellen te truncaten?
Alleen na het invalideren van de bijbehorende indexer en het draaien van een volledige reindex. Truncaten zonder invalideren slaat queued wijzigingen over en laat permanent stale data in de index achter.
Update on save of schedule mode in productie?
Schedule mode voor elke catalogus met regelmatige bulk-imports of prijsupdates. Update on save blokkeert storefront writes tijdens reindex en is de meest voorkomende oorzaak van vertraging midden in checkout.