058 —Magento
Magento 1.9 to Magento 2: a six-weekend live cutover
A Dutch agency had to move a Magento 1.9 store onto 2.4.7 without ever freezing the catalog. This is the six-weekend rebuild plan that actually held.
The Friday night Loom landed at 23:41. A 14-person agency in Utrecht, running a Magento 1.9.4.5 store for a wholesale paint distributor, had ninety days before their PSP stopped signing PHP 7.2 requests and Adobe stopped backporting security patches. The store turned over roughly €2.3M a year. Catalog updates ran every weekday morning from an ERP feed. Freezing the catalog for a long weekend was not on the table, and a Black Friday rebuild definitely was not. We had six weekends to move a live Magento 1.9 store to Magento 2.4.7 without losing an order or a URL rank.
This is the rebuild plan that actually held. The Magento 2 migration playbook below assumes you have working backups, a copy of the production database you can refresh weekly, and at least one engineer who has read the official Adobe Commerce data migration tool docs without skipping anything.
The constraint that ruled out a catalog freeze
The classic Magento 1 to Magento 2 migration tutorial assumes a freeze window: cut writes on the source, run the data migration tool, validate, switch DNS, unfreeze. That is fine for a 200-SKU shop with weekly catalog edits. It is impossible for a wholesale store with a 14,000-SKU catalog, an ERP that pushes pricing every morning, and a B2B portal that takes EUR 8k orders at 23:30.
The data migration tool ships with a delta mode designed for exactly this. After the initial bulk migration, it tails specific tables (sales_order, customer_entity, catalog_product_entity, and a few dozen others) using log tables and triggers installed on the source. As long as the destination Magento 2 instance stays current with the deltas, you can cut over in a fifteen-minute DNS swap rather than a two-day freeze.
The hard part is not running the tool. The hard part is keeping the delta loop healthy for six weeks while developers also rebuild themes, replace extensions, and write the integration shims that the old Magento 1 frontend held together by habit.
Weekend 1: shadow infrastructure and the first bulk dump
The first weekend was infrastructure only. We provisioned a Hetzner box, installed Magento 2.4.7 on PHP 8.2 with OpenSearch 2.12, MariaDB 10.6, Redis 7, and Varnish 7.4. We pointed it at a staging hostname behind basic auth. No traffic yet. No theme yet.
Then we ran the official data migration tool against a fresh production dump. The bulk migration took 4h22m on the first pass and surfaced the usual debris:
php bin/magento migrate:data \
-r vendor/magento/data-migration-tool/etc/opensource-to-opensource/1.9.4.5/config.xmlErrors in the first run looked like this:
[ERROR]: Source documents are not mapped: enterprise_logging_event
[ERROR]: Class "Vendor_Module_Helper_Data" not found
[ERROR]: Mismatch of entities in document: catalog_product_entity_varcharThree patterns to know. The enterprise tables only matter if you used Magento Enterprise; in Open Source they go into ignore blocks in map.xml. The Helper not-found errors come from third-party M1 modules that registered EAV attributes the migration tool then tries to type-map; if you do not need the attribute on M2, drop it from the source schema before the next pass. The mismatch errors usually mean a varchar column was extended on M1 by an extension and the tool wants you to acknowledge it in the map file.
By Sunday night the bulk pass was green and the staging instance had 14,103 products, 28,440 customers, and 142,617 historical orders. None of them less than 48 hours old. That is what the delta loop is for.
Weekends 2 and 3: holding the delta loop
The delta tool runs as a long-lived PHP process:
php bin/magento migrate:delta \
-r vendor/magento/data-migration-tool/etc/opensource-to-opensource/1.9.4.5/config.xmlWe ran it inside a tmux session on the staging box, with a systemd unit watching the process and restarting it on exit. The triggers it installs on the M1 source are non-trivial. Look at what gets created:
SHOW TRIGGERS LIKE 'sales_flat_order';You will see trg_sales_flat_order_after_insert, _after_update, _after_delete, each writing the changed primary key into a m2_cl_* log table. The delta process polls those log tables every minute and replays the changes on M2. If the source goes down for maintenance and the log tables get backlogged, the delta process catches up. If the log tables grow without bound because the delta process is dead, you find out at the worst possible time.
We learned to alert on two metrics: row count of any m2_cl_* table over 5,000, and time-since-last-poll over 180 seconds. Both went into the same Grafana board the ops team already watched.
Weekend 3 was when the second engineer rebuilt the theme. We did not port the M1 RWD theme. Nobody should. We started from Luma, ported the brand tokens, and rebuilt the four templates that mattered: PDP, PLP, cart, and checkout. Everything else inherited.
Weekend 4: URL rewrites and the SEO ledger
The single biggest reason Magento migrations get rolled back is URL drift. The data migration tool will copy your core_url_rewrite rows from M1 into the M2 url_rewrite table, but the schema is not one-to-one. M1 stored category and product rewrites separately from CMS rewrites. M2 unifies them and adds redirect_type.
We pulled the live sitemap from Google Search Console, deduplicated against a fresh crawl, and built a CSV of every URL that had received an organic click in the last 90 days. 11,847 URLs. Then we wrote a small comparison query against staging:
SELECT s.url AS source_url
FROM seo_ledger s
LEFT JOIN url_rewrite u
ON u.request_path = TRIM(LEADING '/' FROM s.url)
WHERE u.url_rewrite_id IS NULL;This returned 312 URLs that existed in production traffic but had no rewrite on M2. About 200 were CMS pages built with a deprecated M1 page builder. The rest were category landing pages with custom URL keys set in the admin years ago and never propagated when SKUs were re-tagged.
For each, we wrote a permanent redirect into url_rewrite with redirect_type = 301, pointing at the closest live page. The site lost zero ranking positions in the four weeks after cutover. That single Saturday of grep and SQL was probably worth two months of post-launch SEO work.
Weekend 5: payments, taxes, and the cutover rehearsal
By weekend 5 the M2 instance was running, the theme was 90% there, and the delta loop had been clean for ten days. The remaining risk was the integration layer: payments, ERP, and tax.
The PSP integration was a custom M1 module written in 2018. We did not port it. The M2 build uses the PSP's official extension, which was released in 2023 and handles 3DS2 properly. We migrated stored card tokens through the PSP's API, not through Magento, which is the only way that does not violate PCI.
The ERP feed was the messy one. The original feed posted XML to a SOAP endpoint exposed by an M1 module. We could have rebuilt the SOAP endpoint on M2, but it was 2026 and we did not want to. Instead we wrote a small Symfony service that sat between the ERP and Magento 2, accepting the legacy XML format and translating it to M2 REST calls. The ERP team did not have to touch a thing. The translation service is 412 lines of PHP and a docker-compose file.
Then we did a full cutover rehearsal. We swapped DNS for an internal test domain, ran a staged checkout from cart through to PSP confirmation, validated the order appeared in the ERP, then rolled back DNS. The rehearsal surfaced one bug: the M2 default tax calculation rounded VAT differently from M1 by 0.01 EUR on certain mixed-VAT carts. We patched the rounding mode in core_config_data (tax/calculation/algorithm set to TOTAL_BASE_CALCULATION) and the diff vanished.
Weekend 6: the cutover itself
The actual cutover took 38 minutes. The runbook:
- Pause the ERP feed at 22:00 Saturday.
- Final delta run, verify zero rows in
m2_cl_*log tables. - Put M1 in maintenance mode.
- Final integrity SQL: order counts, customer counts, product counts match across both databases.
- Swap DNS to the M2 box. TTL had been lowered to 60s the previous Tuesday.
- Smoke checkout, smoke admin, smoke ERP feed (unpaused), smoke search.
- Disable the delta tool's triggers on the M1 source. Leaving them running on a frozen source eats disk.
Step 7 is the one teams forget. Triggers installed by the data migration tool stay there until you drop them. Months later, when someone digs out the old M1 database for analysis, those triggers will happily write to log tables that no longer have a consumer.
DROP TRIGGER IF EXISTS trg_sales_flat_order_after_insert;
DROP TRIGGER IF EXISTS trg_sales_flat_order_after_update;
DROP TRIGGER IF EXISTS trg_sales_flat_order_after_delete;What we kept from the old store
Three things were preserved verbatim and the team was glad we did not modernize them:
- The legacy customer group IDs. The B2B portal had price tier logic keyed to specific integer IDs. We mapped them one-to-one.
- The product URL keys. Not the templates around them. The keys themselves. Search rankings live in those slugs.
- The order increment IDs. Customer service had years of muscle memory around the M1 prefix. We extended the existing sequence rather than starting over.
The wider lesson is that a Magento 2 migration is mostly an exercise in restraint. The temptation to rewrite the data model, refactor the catalog tree, or fix the URL keys is real, and each one of those decisions trades engineering taste for SEO position and customer-service confusion. Pick the smallest possible change set.
Editing the old store while the new one was built
For six weekends the agency team had to keep editing a legacy site while a parallel rebuild ran next to it. That is the part of a Magento migration nobody writes about. Every CMS block touched on M1 has to be reproduced on M2 before cutover. Every catalog rule. Every promotion. The delta tool handles structured data, not editorial.
We tracked these editorial changes in a shared Notion board for the first two weekends and it was a mess. By weekend 3 we had moved to working on the M1 files directly through Pier, which gave us a single audit log of every PHTML, layout XML, and CMS-block edit. When we built Pier we ran into this exact pattern across half a dozen Magento clients, and the way we ended up handling it was a per-file version history tied to a MySQL editor for the cms_block and cms_page tables, so the M2 rebuild had a definitive list of what to mirror with diffs.
The smallest thing to do today
If you have a Magento 1 store still running, the first useful 30 minutes is to dump core_config_data and grep it for serialized PHP. Every block of a:N:{...} is a future fatal on the M2 admin and a known unserialize hazard. Find them now while you have time to choose how to handle each one. The migration tool will be much happier in three months when you actually run it.
— Questions —
How long should a Magento 1 to Magento 2 migration take?
For a mid-size catalog with active orders, plan six to eight weekends of evening work plus one cutover night. Bulk migration is fast; the delta loop and theme work are what consume the calendar.
Can you really run Magento 1 and 2 in parallel?
Yes, using the official data migration tool's delta mode. It installs triggers on the M1 source and replays changes onto M2 every minute, so you only need a short DNS swap to cut over.
What happens to URL rankings during the migration?
They survive if you audit the rewrite table against real organic traffic before cutover. Build a ledger of clicked URLs from Search Console, diff against the new url_rewrite table, and 301 the gaps.
Is the data migration tool still maintained in 2026?
Yes, it ships with current Magento Open Source and Adobe Commerce releases. The config files for 1.9.x sources are still in the vendor package. Read the map.xml docs before your first run.
Do you need to freeze the catalog at any point?
Only for the final cutover window, which is typically under an hour. Everything before that runs live, with the delta loop keeping M2 current against ongoing M1 edits.