Why Your D365 F&O Cutover Is Slower on the “Better” Environment (And What to Do About It)

Why Your D365 F&O Cutover Is Slower on the “Better” Environment (And What to Do About It)

If you have only ever loaded data into a Tier-1 dev box, your first cutover run on a Tier-2 or higher sandbox is going to surprise you. You will assume that the bigger, multi-server, production-architected environment will be faster than the single-box VM you have been working on for the last twelve months. It almost certainly will not be. On identical data volumes and identical packages, imports can take substantially longer on the higher tier. I have seen the same load run several times slower after the move. 

Project managers find this hard to accept. The phrase you tend to hear in cutover stand-ups is “the upgrade has broken something.” It hasn’t. The architecture is just different, and the Data Management Framework (DMF) is sensitive to that difference in ways that don’t show up in normal day-to-day usage. I’ve been involved in a recent cutover where this exact pattern played out, and almost every D365 F&O implementation I have worked on hits some version of this same conversation. 

This is a follow-up to my earlier piece on Establishments in 10.0.48. Same intent: explain the thing the platform doesn’t explain well, and give architects, consultants and PMs something practical they can use. 

Tier-1 and Tier-2+ are not the same machine wearing different hats 

A Tier-1 environment is a single virtual machine. SQL Server runs on the same box as the Application Object Server (AOS). When DMF reads a staging row or writes a target row, the traffic stays on the same VM. There is no network hop in between. That is unusually fast for a typical enterprise application stack, and it bears almost no resemblance to a production deployment. 

A Tier-2 or higher sandbox is a different animal. AOS runs on its own compute, and the database is Azure SQL Database on a separate cluster. Even within the same Azure region, every read and every write crosses a network hop. For a normal user click, you do not feel it. For a DMF job pushing hundreds of thousands of records back and forth between staging and target, every one of those round-trips adds up. Microsoft’s own optimisation guidance is blunt about this: 

“Don’t compare or extrapolate testing results in a Tier-1 environment to performance in a Tier-2 or higher sandbox environment.” 

That sentence is in the official Learn documentation for a reason. 

There is a related point that often gets lost. Microsoft specifies Tier-2+ as the correct environment for performance testing precisely because it represents production behaviour. Tier-1 is fine for development, learning the entity, getting your mappings right. It is not a useful baseline for cutover planning. If your cutover plan is based on Tier-1 timings multiplied by your data volume, throw it away and rebuild it on a Tier-2 or higher sandbox. 

Database logging is the easiest win you are leaving on the table 

D365 F&O lets you track inserts, updates and deletes against specific tables via the database log. It is a useful audit feature. It is also expensive when you are loading data, because every insert into a logged table fires a SQL trigger that writes a row to SysDatabaseLog, and the trigger runs inside the same transaction as the original write. You cannot push 400,000 customers into a logged table without paying for 400,000 trigger executions and 400,000 audit rows. 

For data migration runs, switch logging off on every table you are loading. Capture the list, document it, and turn it back on after cutover. If you have logging enabled on heavily customised master data, say a pricing table with a lot of dependent setup, the overhead is worse than you think, because the trigger fires on every row touched by every parallel task. 

This won’t fix the architectural latency I described above. It will, however, take a real chunk out of your runtime, and it costs nothing. 

Entity parallelism: two things have to be true at once

This is the part that catches teams out most often. To get parallel processing on a DMF import, two conditions both need to hold. Not one. Both. 

The first is that the entity itself has to support multi-threading. Many standard entities don’t. The clearest example is Customers V3. If you try to configure a task count above one for that entity, the system rejects it: “Custom sequence is defined for the entity ‘Customers V3’, more than one task is not supported.” That is because the entity has a custom number sequence in its definition, and parallel threads cannot safely share a sequence without risking duplicates or gaps. 

D365 F&O error message: Custom sequence is defined for the entity Customers V3, more than one task is not supported`

D365 F&O error message: Custom sequence is defined for the entity Customers V3, more than one task is not supported`

The fix on the customer side is to use the Customer definitions entity (CustCustomerBaseEntity) instead. It is a leaner entity, with fewer data sources, no self-relation, and it does support parallelism. Combined with Customer details V2 for the secondary attributes, you get the same end-state with multi-threaded throughput. The performance delta between Customers V3 and the definitions/details combination is genuinely large; the Microsoft documentation describes it as an order of magnitude. 

The second condition is that the framework has to be told what “parallel” actually means for that entity. That is the bit teams forget. You configure it in Data management > Framework parameters > Entity settings > Configure entity execution parameters, and you set two values per entity: the Import threshold record count (how many records per thread) and the Import task count (how many threads to use above that threshold). 

There is no universal correct answer. The right values depend on the entity, the table behind it, the volume of data and the size of your environment. For a small reference table, parallelism may not help at all. For a wide custom entity with heavy validation, you may want a higher record count per thread and a moderate task count. The only way to find out is to test. 

Entity import execution parameters form, showing one or two configured entities with explicit Import threshold record count and Import task count values

Entity import execution parameters form, showing one or two configured entities with explicit Import threshold record count and Import task count values

DMF tuning is a loop, not a setting

The healthiest way to think about cutover performance tuning is iterative. Pick the entities that actually drive your runtime (usually a handful of master data and opening balance entities, not all 200 in your package) and run them at small volumes first. 1,000 records, then 10,000, then 100,000, then full. Capture the time, the task count, the threshold, the batch group, the maximum batch threads on the AOS, and whether business validations and field validations were on or off. Vary one thing at a time. Build a table of what worked. 

While you are doing that, set up a dedicated batch group for your migration job and use Priority-Based Batch Scheduling to give it the priority and reserved capacity it needs. Note that the old “assign batch servers to a batch group” model is gone. Once Priority-Based Batch Scheduling is enabled (which is the case on current platform versions), the Batch servers tab on the batch group form disappears, and batch groups control priority and maximum concurrency rather than server affinity. Increase Maximum batch threads on each AOS from the default of 8 toward 12 or 16, but only if you have headroom and you’ve tested that the box can take it. Disable change tracking on the entities you are loading. Where the entity supports it, enable set-based processing. For the final cutover run on data you trust, look at the per-data-source flags on the Entity structure form. Run business validations controls whether validateWrite() on the underlying tables (and any event handlers on it) fires during the import. Switching it off is comparatively safe once you’ve already proven the data is clean. 

Run business logic in insert or update method is a different conversation. It controls whether the table’s insert() and update() methods (and any event handlers wired to them) run on each row. Disabling it can be a meaningful win on entities where those methods are heavy, but it also means you skip whatever cascading logic they perform: dependent table updates, derived field recomputation, default values, downstream side-effects. Don’t tick it off unless you have specifically traced what depends on those methods for the entity you’re loading and confirmed your migration genuinely doesn’t need it. Those toggles exist for a reason, and the second one is the riskier of the two by a long way. 

Entity structure form showing Run business validations and Run business logic in insert or update method checkboxes at the data source level

Entity structure form showing Run business validations and Run business logic in insert or update method checkboxes at the data source level

A practical checklist for a first Tier-2+ migration

Before your first big run on the higher-tier environment, walk through this. None of it is exotic; all of it is missed regularly.

  • Treat Tier-1 timings as engineering reference data only, never as a basis for cutover scheduling.
  • Validate every runtime estimate on a Tier-2 or higher sandbox that mirrors production.
  • Disable database logging on every table involved in the migration, and document the list so you can re-enable cleanly afterwards.
  • Replace Customers V3 with Customer definitions plus Customer details V2 if you need parallelism on customers, and audit the rest of your master data entities the same way.
  • Configure entity execution parameters explicitly for every entity that matters; without an entry, the import runs single-threaded regardless of how big the environment is, and the whole point of being on Tier-2+ is lost.
  • Run imports in batch mode, on a dedicated migration batch group, with a sensible Maximum batch threads setting on each AOS.
  • Iterate at small data volumes first.
  • Disable change tracking.

On the final, fully validated run, consider switching off Run business validations on the entity structure, and only consider switching off Run business logic in insert or update method if you have proven nothing downstream relies on it. Schedule a mock cutover and treat its timings as the real plan.

If your project team is going through this for the first time and the numbers feel wrong, they probably are not. You are just measuring an architecture you weren’t measuring before. Plan for it, tune for it, and the cutover window becomes manageable. 

Sources: 

Microsoft Learn — Optimize data migration for finance and operations apps 

Microsoft Learn — Customer definitions entity 

Microsoft Learn — Customers V3 entity 

Microsoft Learn — Priority-based batch scheduling