The default has been ReadUncommitted all along

Business Central 2026 release wave 2 adds a ReadState property to AL queries, so a query can be told to read only committed data. The default stays ReadUncommitted, where it has always been. Every AL query in every extension you maintain has been reading uncommitted data, whether or not the person who wrote it made that choice on purpose.

The report comes from Dynamics 365 Lab, an independent Business Central MVP blog, in a post published on September 21, 2026. This is a personal blog rather than a Microsoft newsroom, and no vendor statement or quote accompanies the change. Microsoft Learn preview documentation for update 29.0 corroborates the feature, listing the ability to read only committed data in AL queries and to choose the isolation level for isolated-storage reads.

Four levels exist. ReadCommitted, ReadExclusive, ReadShared and ReadUncommitted, with ReadUncommitted the one queries have used until now. The blog frames the change as parity with the ReadIsolation method that arrived for Record variables in Business Central 22.

What a dirty read costs a posting routine

A dirty read means your query can return a row another session has written inside a transaction that has not committed yet. If that session errors and rolls back, the row you read never existed. Your query already returned it, your code already added it to a total, and nothing afterwards tells you the number was built on a row that got thrown away.

Business Central developers write reporting and validation logic, and that logic runs while other users are posting, which is the normal state of a Dynamics 365 Business Central install at month end. A credit check that sums outstanding entries, or a threshold test that raises an error and stops a document, reads while somebody else is halfway through a posting routine that may or may not finish.

ReadCommitted removes the rolled-back row from that picture. It does not promise the rows stay consistent for the whole transaction, so a value read at the start of your logic can still differ from the same value read at the end. The source is explicit about that limit, and it matters if you read the same data twice and compare.

Records got this in version 22 and queries did not

The parity point tells you where the inconsistency has been hiding in your own code. ReadIsolation arrived for Record variables in Business Central 22, so a developer who cared about this had a way to set it on a record loop years ago. Set it on the record, get nothing on the query, and the two halves of the same codeunit end up reading under different rules.

That pattern is common in extensions that grew over several releases. Someone tightened the record loop after an incident, left the query aggregate alone because there was no property to set, and the mixed behaviour survived every review since. Nothing in the compiler flagged it, and no test would show it unless two users collided.

Check your own repo for that shape before deciding anything else. If a codeunit sets ReadIsolation on a Record and also runs a query over related data, the query has been the looser of the two. Code other people can maintain starts with the parts where two halves of one routine disagree about what they are reading.

Stricter is the wrong default to reach for

ReadUncommitted earns its place. Business Central prizes throughput, and readers that never block writers keep posting routines moving during the hours when everyone posts at once. A platform that applied ReadCommitted everywhere would trade that away for every query in every extension on the tenant.

Our read is that moving an entire extension to ReadCommitted buys correctness you mostly do not need and costs concurrency you do. Stricter isolation makes readers and writers wait on each other more often, and the waiting shows up in the posting routine a user is standing in front of.

Neither the blog nor the documentation says anything about the cost of the stricter levels under load, and no benchmark exists to quote. That is the part we would test on a copy of production before rolling a change across an extension, with a release checklist that names who verifies it.

Decide per query, based on what the answer is for

The useful question is what happens to the number after your query returns it. A query feeding a figure somebody acts on deserves different treatment from a query feeding a list somebody scrolls. Most queries sit clearly at one end or the other, and the rest are what review is for.

A credit limit check, a reconciliation total, an approval threshold, a quantity that decides whether a shipment goes out. Those turn into an action, and an action taken on a phantom row becomes a support call nobody can reproduce, the same failure mode as the metric nobody can reproduce twice.

Lookup lists, filtered datasets behind a page, search results, anything a user reads before acting through a separate operation that does its own checking. Leave those alone. A stale row in a list gets corrected the moment somebody picks it and the real validation runs.

The review worth running this week

Open the extension and list every query object. For each one, find the caller and ask whether the result reaches a conditional, an Error, a field assignment or a posting decision. That list is short in most extensions, and it is the only list that needs a ReadState decision when update 29.0 reaches your tenant.

Then check the surrounding code for ReadIsolation on Record variables. Anywhere the record is tightened and the query is not, half of that routine has been reading under looser rules than its author intended. Fix those first and leave the rest at the default.

The wider question for architects is where this validation belongs at all, which we covered in where business logic should live. A posting rule that only holds when nobody else is posting was never a rule, and a ReadState property makes that visible in code for the first time.