SaaS · Performance

Acumatica Performance — Cache Tuning

Acumatica Performance — Cache Tuning is the Acumatica performance topic that nobody asks about until they have to.

John Kihiu12 min read

"Cache tuning" in Acumatica means two genuinely different things depending on who is asking, and conflating them wastes time: there is PXCache, the per-graph, per-request in-memory object cache that every DAC row passes through, and there is IIS output/static caching, the HTTP-level caching of static assets and, on some builds, response output. Neither is Redis, and neither is a general-purpose distributed cache you configure once and forget — I will cover the actual gap where Redis fits, or does not, in the Redis-specific post in this series.

PXCache is not a knob, it is the data access layer

Every BQL Select against a graph's data views goes through PXCache, which holds recently touched rows in memory for the life of the graph instance and dedupes repeated selects against the same key within a request. You do not "enable" PXCache — it is always on — but you can defeat its benefit or actively fight it:

C# — reuse what is already cached
// Wasteful: re-queries a row the cache already has
protected virtual void SOOrder_RowUpdated(PXCache cache, PXRowUpdatedEventArgs e)
{
    var order = (SOOrder)PXSelect<SOOrder,
        Where<SOOrder.orderNbr, Equal<Required<SOOrder.orderNbr>>>>
        .Select(Base, ((SOOrder)e.Row).OrderNbr);
    // ...
}

// Better: the row is already in hand
protected virtual void SOOrder_RowUpdated(PXCache cache, PXRowUpdatedEventArgs e)
{
    var order = (SOOrder)e.Row;
    // ...
}

Cross-graph cache invalidation: the part people forget

PXCache is per-graph-instance, not a single shared store — which means a change made through one graph does not automatically appear in another already-instantiated graph's cache within the same request pipeline unless you explicitly propagate it. Customizations that manipulate a sibling graph's data directly (rather than through its own actions/events) can leave that sibling graph's cache holding stale rows for the remainder of the transaction. Use PXCache.Update/Insert through the owning graph's own cache, not a raw SQL-adjacent shortcut, so the invalidation actually happens where it needs to.

PXLongOperation runs in its own graph context

A long-running processing operation started via PXLongOperation.StartOperation executes against its own graph instance, separate from the UI thread's graph. If the UI-side graph is still holding cached rows the long operation modifies, the user's screen can display stale data until they navigate away and back, refreshing the cache. This is expected behavior, not a bug — but it is worth setting user expectations for during UAT rather than fielding a "the record didn't update" ticket after the fact.

The other cache: IIS static/output caching

Separately, IIS static content caching (for the compiled JS/CSS bundles, images, fonts under the site's static folders) should be on with a long max-age, since Acumatica versions its static asset URLs on upgrade, invalidating the cache automatically when the version changes. This is unrelated to PXCache and is pure network/browser-level caching — worth checking with the same browser dev-tools network tab used for the compression post in this series, looking for Cache-Control headers on static assets.

Wrapping up

"Cache tuning" on Acumatica mostly means writing customization code that respects PXCache instead of fighting it — reuse rows you already have, avoid indiscriminate cache.Clear(), and propagate changes through the owning graph. IIS static asset caching is a separate, simpler win. Neither of these is a distributed cache layer; if the requirement is genuinely a shared cache across app servers, that is custom integration territory, covered in the Redis post.

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.