Acumatica · Performance

Acumatica Performance — Async Loading Patterns

Acumatica Performance — Async Loading Patterns is the Acumatica performance topic that nobody asks about until they have to.

John Kihiu12 min read

Acumatica has two genuinely different "async" stories, and mixing them up is how customizations end up either blocking the UI unnecessarily or, worse, corrupting data by racing against a request that has not actually finished: PXLongOperation for long server-side work kicked off from the UI, and ordinary async/await in .NET for I/O-bound custom code like external API calls. Neither is "fire and forget the way JavaScript promises are" — both have Acumatica-specific rules.

PXLongOperation: the platform's answer to "this button takes thirty seconds"

Any action that would otherwise block the UI thread for more than a couple of seconds — bulk processing, a report-heavy calculation, an integration call to an external system — belongs behind PXLongOperation.StartOperation, which runs the work on a background thread against its own graph instance and lets the UI poll for completion with a progress indicator, rather than holding the HTTP request open.

C# — starting a long operation correctly
public PXAction<SOOrder> RecalculatePricing;
[PXButton]
[PXUIField(DisplayName = "Recalculate Pricing")]
protected virtual IEnumerable recalculatePricing(PXAdapter adapter)
{
    var orderNbr = Base.Document.Current?.OrderNbr;
    PXLongOperation.StartOperation(Base, () =>
    {
        // Runs on a background thread with its own graph instance —
        // do not close over Base's cache rows here, re-select fresh.
        var graph = PXGraph.CreateInstance<SOOrderEntry>();
        graph.Document.Current = graph.Document.Search<SOOrder.orderNbr>(orderNbr);
        RecalcAllLines(graph);
        graph.Save.Press();
    });
    return adapter.Get();
}

The mistake I see most often: closing over the calling graph's cache objects (Base.Document.Current) directly inside the background delegate instead of creating a fresh graph instance and re-selecting. The background thread executes after the original request has already returned to the browser, so the original graph's row objects may be stale or, in concurrent scenarios, mutated by something else entirely by the time the background code runs.

Do not nest PXLongOperation calls

Starting a long operation from inside another long operation's delegate does not parallelize the work — it typically throws or silently no-ops depending on the build, because the long-operation manager is scoped per originating request. If a background operation needs to fan out into multiple independent units of work, loop and process them sequentially within the one long operation, or use genuine Task-based parallelism inside the delegate with your own bounded concurrency, not nested PXLongOperation calls.

Real async/await: for I/O, not for UI responsiveness

Custom code calling an external REST API, sending an email, or writing to blob storage should use standard .NET async/await for that I/O — but be deliberate about where the awaiting happens. A synchronous graph event handler (RowUpdated, FieldUpdated) that blocks on .Result or .Wait() against an async call risks thread pool starvation under load, since it ties up a worker thread waiting instead of releasing it. Where the framework's method signature supports genuinely async event handlers, use them; where it does not (much of the older graph event surface is synchronous by design), keep the awaited call short and consider whether it belongs inside a PXLongOperation instead if it might be slow.

The UI side: what "async" looks like to the user

A PXLongOperation-backed action returns immediately and the client polls a status endpoint, showing a progress indicator until the operation reports completion or failure. Custom processing screens built on PXProcessing<T> get this polling and progress UI for free — which is the reason to prefer building bulk-action screens on the processing screen pattern rather than a bespoke button-plus-long-operation combination whenever the UI shape fits (a list of records, a mass action, a progress bar).

Wrapping up

Reach for PXLongOperation whenever a UI-triggered action would otherwise block for more than a couple of seconds, always re-select data fresh inside its delegate rather than closing over the calling graph's cache, and never nest long operations. Use standard async/await for I/O-bound custom code, but keep synchronous event handlers from blocking on it under load.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.