Back to the journal

engineering / Maintra journal

Why is Maximo slow? A field guide

Eight causes behind a slow Maximo JVM, where each shows up in the database, logs and thread dump, and how to read them together, by hand or with an agent.

Graphite and ink cutaway of an industrial pump room: one pump fed by a manifold of valves, one valve dripping, the pressure gauge high.

Monday, 09:40. The service desk says Maximo is slow. CPU on the UI server is at thirty percent, and a planner at one site is watching a work order save take sixteen seconds.

The administrator opens SystemOut.log. The DBA opens the session list. The developer opens a thread dump. Each of them is looking at a real piece of the problem, in a different tool, in a different vocabulary, and none of the pieces says “WOTRACK save, user WHITE, custom class WOCostService, line 16”.

Somebody suggests a restart. The symptom clears, the evidence is gone, and it happens again next Monday.

This is what I check instead, in the order I check it. It applies to Maximo on WebSphere and, with the same JVM option, to Liberty and Manage.

Where slowness actually lives

“Slow” hides four different situations. Work out which one you have first, because the first place to look is different for each.

What people reportFirst place to lookWhat it usually is
Everyone is slow, and getting slowerConnection pool and heap after full GCLeaked connections or a heap that never comes back down
Everyone is slow at the same times of dayCron Task Setup history and the thread dumpEscalations, reorder or a custom cron running in the UI JVM
One screen or one action is slow for everyoneSQL log filtered by that actionRelationship in a loop, wildcard search, unfiltered list
Nothing responds at allThread dump, then the database’s blocking sessionsLock convoy, deadlock, or every thread waiting on one blocked session
The scattered evidence on one screen. Capture from MXRay running against its Maximo simulator, not a customer system. Open full size.

Connections that never come back

Maximo runs its own pool through psdi.server.DBManager. Code that calls getConnection(connectionKey) must call freeConnection(connectionKey) in a finally block. If an exception escapes between the two, that connection is gone until restart, and nothing logs it.

The fingerprint is gradual degradation, then a cliff. The database session count for that JVM host climbs and stays up, and the thread dump shows WebContainer threads waiting inside DBManager.getConnection. The culprits are custom field classes and automation scripts that open a connection for direct JDBC.

Search for getDBManager().getConnection and pair every one in finally. Until the fix ships, a rolling restart before the pool runs dry is the honest stop-gap.

Each leak with the user, the action and the line that took it. MXRay against the simulator; the custom class is a planted bug. Open full size.

Memory: oversized MboSets and the GC settings

Every business object is a full Java object graph of five to twenty kilobytes. A list tab opened without a filter on a hundred thousand assets asks the JVM to build a hundred thousand of them, and fifty thousand MBOs in one set can take a gigabyte of heap and stall everyone through a long collection.

Maximo ships the safety net: mxe.db.fetchStopLimitEnabled=1 with mxe.db.fetchResultStopLimit=5000. It is enabled by default and switched off at a surprising number of sites after one complaint. Per-object exceptions go in mxe.db.fetchResultStopLimit.<OBJECTNAME>.

Give list tabs a default query, then turn the limit back on with Live Refresh. Code that iterates a large set read-only should call setFlag(MboConstants.DISCARDABLE, true) and work in batches.

On the JVM side, four settings cause most of the trouble on IBM J9: initial and maximum heap equal, -Xgcpolicy:gencon, a nursery of a quarter to a third of the heap, and -Xverbosegclog switched on. The GC log answers the question that matters: after a full collection, does the heap come back down to where it was yesterday? If the floor rises day after day, something is holding MBOs, usually a static field, a session attribute or a cache in custom code. Take a heap dump while the JVM is up, open it in Eclipse Memory Analyzer and sort by retained size.

A live, attributed mxe.mbocount: which object, how many, which user, and the where clause that loaded them. MXRay against the simulator. Open full size.

SQL that multiplies inside custom code

The most common reason one action is slow is the N+1 pattern: a save loops over a set and calls mbo.getMboSet("ASSET") inside the loop, so the relationship query runs once per row. Two hundred work order lines become two hundred round trips. Sort the database’s top SQL by executions rather than elapsed time and it stands out; the fix is to fetch once, outside the loop, or to pull the attribute through a relationship on the parent set.

Two neighbours are worth checking at the same time. An attribute whose Search Type is WILDCARD turns every list filter into LIKE '%value%', which no index can serve; set code fields such as WONUM and ASSETNUM to EXACT and descriptions to TEXT with a text index. And a relationship or list tab with no where clause scans the table and builds the oversized set from the previous section.

Every statement attributed to the action and user that ran it, with N+1 and wildcard patterns flagged. MXRay against the simulator. Open full size.

Cron tasks in the users’ JVM

If the slowness has a schedule, look at the schedule. Escalations, PM generation, reorder, JMS consumers and every custom cron run on cronAction() in whichever JVM picks them up, and on a small install that is the JVM the planners are using. An escalation that qualifies four thousand records on the hour takes the heap and the pool with it.

Cron Task Setup shows the history per instance; overlapping runs mean the interval is shorter than the run time. The layout IBM has recommended for years is the fix: dedicated JVMs for UI, cron, integration and reports, with mxe.crontask.donotrun=ALL on the UI JVMs. Any system that lets interactive and background work share one runtime has this problem eventually, which is why Maintra runs background jobs in their own workers, sized apart from the API people click on.

Every cron run with its SQL and MBO cost, and a custom task failing against an external endpoint. MXRay against the simulator. Open full size.

Everyone blocked on one logger

This one looks like a database problem. Someone raised the sql, mbo, crontask or integration root logger to DEBUG and never put it back. Log4j serialises writes through the appender, so every thread that logs now queues behind one lock. The thread dump shows dozens of threads BLOCKED on the same owner, and the owner is inside the logging framework.

The fix takes two minutes and no restart: Logging application, root loggers back to ERROR or WARN, Apply Settings. If the owner is in your own code instead, it is a synchronized block in a custom class, usually a cache or a counter.

Nearly half the sampled time inside the log appender, with an automation script visible below it. MXRay against the simulator. Open full size.

Hung threads and the database behind them

WebSphere reports a hung thread with WSVR0605W after ten minutes by default. The users have been waiting ten minutes too. On IBM J9, kill -3 <pid> writes a javacore without stopping anything; take three, thirty seconds apart. Read the LOCKS section first for a named deadlock, then the BLOCKED threads and their owner, then the threads inside socketRead0 under a JDBC frame. If most WebContainer threads are in that last group, the JVM is waiting on the database, and the database will name the blocking session:

-- Oracle
select sid, blocking_session, event, seconds_in_wait from v$session where blocking_session is not null;
-- Db2
select * from table(mon_get_appl_lockwait(null, -2));
-- SQL Server
select session_id, blocking_session_id, wait_type, wait_time from sys.dm_exec_requests where blocking_session_id <> 0;

The blocking session is very often a Maximo transaction that is itself stuck in the JVM, which is why the thread dump and the session list have to be read together. IBM documents the hang detection policy; lowering the threshold gets the warning earlier, but a dump gets the cause.

The first thirty minutes

Before anyone restarts anything

  1. Keep the evidence Three javacores thirty seconds apart, the current verbose GC log, and the database session count per application server host.
  2. Decide JVM or database Count threads in socketRead0 under JDBC against threads in Maximo or custom frames. Mostly database: blocking sessions. Mostly JVM: continue.
  3. Check memory and the pool Heap after the last full collection, pause times, connections held against pool size. A climbing floor or a climbing pool tells you which section above applies.
  4. Find the action and the user From the dump or the SQL log, identify the application, the event and the user behind the busy threads. One action for many users means code; one user means a query.
  5. Fix the cheapest thing first Loggers to WARN, fetch stop limit on, a default query, a cron moved off the UI JVM. Write down what changed so next week’s evidence means something.
Step four, continuously: each Maximo action with its cost and the users behind it. MXRay against the simulator. Open full size.

Let an agent read it

Every capture in this article comes from MXRay, a Java agent I built to do the joining above from inside the Maximo JVM. It attaches through one JVM option or live to a running server, changes nothing in Maximo, and attributes SQL, MBOs, connections and thread time to the action, the user and the custom class or automation script responsible. When something goes critical it writes an incident snapshot to disk, so the evidence survives the restart.

The part that changes how the triage gets done is that every endpoint the dashboard uses is an API. The Doctor tab is /api/doctor, the connections tab is /api/db, the thread X-ray is /api/threads, and there is a compact /api/brief and an /llms.txt that explains the method. An AI assistant can query those directly, cross-reference the leaked connection with the slow action and the failing cron, and write the report with the fix steps, without screen-scraping and without a person copying numbers between tools. The same JSON feeds Prometheus for Grafana and a downloadable snapshot for a support ticket.

It is in early access for Maximo on WebSphere; the details and the request form are here.

The reasoning in this article, run every ten seconds, with the fix steps and the mxe.* values attached. The same data is one GET away for an agent. MXRay against the simulator. Open full size.

Maximo is rarely slow everywhere at once. Something specific is slow, and everyone else is queued behind it.

Haniffa Sajahan Maximo Technical Architect

Frequently asked questions

Frequently asked questions

Why is Maximo slow for everyone at the same time?
Because one shared resource is exhausted: the database connection pool, the heap, a lock in the logging framework, or the database itself behind a blocking session. Take a thread dump and read where the threads are waiting. The shared resource is the frame they have in common.
Should I just give the JVM more heap?
Only after the GC log shows the heap is genuinely full after a full collection. A leak or an oversized MboSet fills any heap you give it, and a larger heap makes each full collection pause longer. Find what is holding the memory first.
How do I find which user or which screen is slow?
Set mxe.db.logSQLTimeLimit so slow statements reach the Maximo log with the user and application, or read the Maximo application and event in the busy threads of a dump. Sorting database top SQL by executions rather than time finds the N+1 loops that per-statement views miss.
Is Maximo slow because it is old, and would replacing it fix this?
No. Every cause in this guide is fixable inside Maximo, and a replacement inherits the same customisations if they are migrated as they are. Replace an EAM because the operating model no longer fits, not because of a leaked connection. If you are evaluating anyway, this is how we suggest running that evaluation, with performance as one criterion among several.

For Maximo teams

See the cause before the next restart.

MXRay is a Java agent that reads your Maximo JVM and names the user, the action, the SQL and the custom class behind the slowness. Every screen it draws is also an API, so your AI assistant can read it too. Early access for Maximo on WebSphere.

Request MXRay early access