With the default configuration, a heap limit of 1 GB, the ramp was unambiguous: up to about 12 concurrent reports the application worked without a flaw, at 14 the heap ran out, and at 20 half of the requests failed. The log entries below are quoted as the logs wrote them; only the file path and the address in the thread name are generalised.
The exact log entries
The application log, application.log, names the place in the CFML code: line 24 is the query that reads from the database, lines 33 and 36 build the report structure in memory. Several worker threads fall over in the same second, which shows that concurrency is the cause:
"Error","ajp-nio-...-exec-1","10/17/26","14:30:21","","Java heap space The specific sequence of files included or processed is: /app/wwwroot/report.cfm, line: 24"
"Error","ajp-nio-...-exec-13","10/17/26","14:30:21","","GC overhead limit exceeded The specific sequence of files included or processed is: /app/wwwroot/report.cfm, line: 36"
"Error","ajp-nio-...-exec-2","10/17/26","14:30:21","","GC overhead limit exceeded The specific sequence of files included or processed is: /app/wwwroot/report.cfm, line: 33"
The exception log, exception.log, confirms the type of error in both variants:
java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: GC overhead limit exceeded
The full stack trace in coldfusion-error.log shows the detail that matters most. The top frames are in the MariaDB driver reading the result packet; below them the query layer of ColdFusion:
SEVERE: Servlet.service() for servlet [CfmServlet] in context with path [] threw exception [ROOT CAUSE:
java.lang.OutOfMemoryError: Java heap space
at org.mariadb.jdbc.client.socket.Reader.readPacket(Reader.java:119)
at org.mariadb.jdbc.client.result.CompleteResult.<init>(CompleteResult.java:100)
at org.mariadb.jdbc.message.ClientMessage.readPacket(ClientMessage.java:373)
at org.mariadb.jdbc.client.impl.StandardClient.readResults(StandardClient.java:1463)
at org.mariadb.jdbc.client.impl.StandardClient.readResponse(StandardClient.java:1382)
at org.mariadb.jdbc.client.impl.StandardClient.execute(StandardClient.java:1306)
at org.mariadb.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java:270)
at coldfusion.server.j2ee.sql.JRunPreparedStatement.execute(JRunPreparedStatement.java:98)
at coldfusion.sql.Executive.executeQuery(Executive.java:1461)
at coldfusion.sql.SqlImpl.execute(SqlImpl.java:368)
at coldfusion.tagext.sql.QueryTag.executeQuery(QueryTag.java:1175)
At the moment of the error the request was fetching the query result and materialising it in memory. An open query keeps the request active, and a large result set pulled into the heap tips it over when many such requests run at once. The same pattern shows in the frames that build the report structure, lines 33 and 36: the heap runs out wherever memory happens to be allocated.
"GC overhead limit exceeded" comes before "Java heap space"
The logs did not start with the message everyone searches for. First, many times, came the other variant of the same error, GC overhead limit exceeded. It means the garbage collector spent more than 98 percent of its time recovering less than 2 percent of the heap: the application was formally alive and practically standing still. Only later came the hard Java heap space. Whoever searches the logs for that phrase alone misses the earlier and far more frequent warning.
The error hits below ColdFusion's limit of concurrent requests
The error appeared at 14 concurrent requests, below the default limit of 25 simultaneous template requests. The conclusion is not intuitive: raising that limit does not help, it lets more heavy requests use the heap at the same time and brings the failure sooner. Throttling requests does not replace a correctly sized heap.
Rising memory use is not yet a leak
After the load the heap returned to about 420 MB. After the fix, with the large heap, the resting use could show 4.5 GB, which looks like a leak at first sight. After a forced garbage collection the live set fell to about 140 MB; the rest was uncollected garbage, because with a large heap the collector is in no hurry. The distinction matters: had the cause been a leak, a larger heap would only have postponed the failure.
A heap dump on a full partition is a dump that never arrives
We enabled the heap dump on an out-of-memory error, so that every future failure leaves a file to analyse. The trap: with a 20 GB heap the dump can take more than ten gigabytes, and the system partition rarely has that much free. The dump that is needed most, after the failure, would quietly not be written. The dump path therefore points to a volume with real spare capacity.
A 20 GB limit, and the JVM shows 18.2 GB
After the limit was set to 20 GB, ColdFusion reported a maximum heap of about 18 205 MB. That is not an error and not lost memory: with the parallel garbage collector the figure the application sees leaves out one of the survivor spaces. The missing two gigabytes are in place, they just do not count towards the visible limit. The same effect shows up at the default heap, which reports about 911 MB of 1024, as described in the installation project.