"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:
- Re-selecting the same row repeatedly with a fresh
PXSelectinstead of reading it once and reusing the cached instance re-hits the query pipeline even though the cache would have served it. This is the single most common inefficiency I find in custom event handlers — aRowUpdatedhandler that doesPXSelect<...>.Select(this, key)when the row it needs is already sitting ine.Rowor reachable viacache.Current. - Calling
cache.Clear()mid-operation for reasons unrelated to genuinely needing a fresh read discards everything PXCache was holding for that graph, forcing full re-fetches on the next access. I have seen this used as a blunt fix for a stale-data symptom that had a different real cause — it "fixes" the symptom by making everything after it slower.
// 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.
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.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.