The best way to kill Business Central performance with a single line of code

In this video, I take a look at a sure way to kill Business Central performance, with a single line code.

https://youtu.be/4AK0NIVyBsY

In this video, Erik demonstrates one of the most common and devastating performance anti-patterns in Business Central development: placing a Modify call inside the OnAfterGetRecord trigger. While it may seem harmless during solo testing, this single line of code can bring a multi-user environment to its knees with deadlocks and cascading locks.

The Dangerous Pattern

The pattern Erik is warning about is deceptively simple. You extend a page — such as the Customer List — and add an OnAfterGetRecord trigger. Inside that trigger, you perform some calculation and then call Rec.Modify() to save the result back to the table.

Here’s what the problematic code looks like:

pageextension 50100 CustomerListExt extends "Customer List"
{
    trigger OnAfterGetRecord()
    begin
        Rec."Credit Limit (LCY)" := VeryIntricateCreditCalculation();
        Rec.Modify();  // THIS IS THE PROBLEM
    end;
}

Why It Seems Fine at First

When you’re developing and testing alone, this pattern works perfectly. You scroll through the customer list, credit limits get recalculated and saved, and everything looks great. The data updates exactly as intended. You might think: “Job done, time to hit the pub.”

But here’s what’s actually happening under the hood:

  • When a non-editable list page retrieves records, it does so in a FindSet-style read operation optimized for speed.
  • By calling Modify(), you’re injecting write transactions into what should be a read-only operation.
  • These write transactions are triggered by the UI — every time a record is displayed, a write lock is acquired.

The Multi-User Catastrophe

The real problems emerge once multiple users are on the system:

  • Deadlocks: One user scrolls down to a record while another scrolls up to the same record. Both try to acquire write locks, creating the perfect storm for a deadlock.
  • Cascading locks: Users scrolling through the list generate overlapping write transactions, causing extended lock chains that bring everything to a halt.
  • Escalating severity: The bigger the table, the harder the system falls. More records mean more lock contention and more opportunities for conflict.

You won’t see the problem during development. You’ll see it as soon as real users start working with the system.

The Right Way: Calculate Without Modifying

Option 1: Calculate Directly in the Page Field

If you just need to display a calculated value, add a field to the page layout and compute it on the fly:

pageextension 50100 CustomerListExt extends "Customer List"
{
    layout
    {
        addbefore(Balance)
        {
            field(OurCredit; VeryIntricateCreditCalculation())
            {
                Caption = 'Credit Limit';
                ApplicationArea = All;
            }
        }
    }
}

This approach calculates the value every time Business Central needs to display the field — no database writes, no locks, no problems.

Option 2: Use a Global Variable in OnAfterGetRecord

Alternatively, you can populate a global variable in the OnAfterGetRecord trigger and bind it to a page field:

pageextension 50100 CustomerListExt extends "Customer List"
{
    layout
    {
        addbefore(Balance)
        {
            field(OurCredit; CreditLimit)
            {
                Caption = 'Credit Limit';
                ApplicationArea = All;
            }
        }
    }

    trigger OnAfterGetRecord()
    begin
        CreditLimit := VeryIntricateCreditCalculation();
    end;

    var
        CreditLimit: Decimal;
}

The global variable stays in memory and gets recalculated for each record as the user scrolls. Some developers prefer this approach because it makes the variable part of the page’s dataset, but both options achieve the same goal: calculating on the fly without saving to the table.

What If You Truly Need to Update the Data?

If you genuinely need to persist calculated values back to the database, the UI can never be the trigger. You need to find a different mechanism — a job queue entry, a codeunit called from an action, or some other process that runs outside the context of page rendering. The key principle is that the act of displaying data should never cause writing data.

The Concept of Read-Only Triggers

Erik points out an important nuance of AL development: the language doesn’t have an explicit BEGIN TRANSACTION / COMMIT model the way SQL does. Transactions are managed behind the scenes — the system initializes them when needed, and you have Commit but no explicit way to begin a transaction.

Because of this, there’s no formal mechanism to mark code as “read-only.” However, you should mentally treat certain triggers as read-only for optimal performance:

  • OnAfterGetRecord
  • OnAfterGetCurrRecord
  • Any trigger whose purpose is to prepare data for display

These triggers exist to prepare data to be displayed, not to modify it in the database.

Summary

The single most destructive line of code you can write in a Business Central page extension is Rec.Modify() inside OnAfterGetRecord. It works perfectly in isolation but creates deadlocks and cascading lock issues in production with multiple users. Instead, calculate values on the fly using page field expressions or global variables, and never trigger write operations from the UI rendering pipeline. If you need to persist data, use a background process or a deliberate user action — not the act of scrolling through a list.