The ColdFusion application failed at peak hours with 30 GB of RAM free: the JVM heap, measured, fixed and proven

An Adobe ColdFusion application ran without complaint at low traffic and returned errors and slowed down at peak hours. We reproduced the problem on a real database, measured the exact point at which the JVM heap runs out, and fixed the memory configuration, with the same load test before and after as the proof.

  • Error threshold measured: 14 concurrent requests
  • After the fix: 0 errors at 20 concurrent requests
  • The server had 30 GB of RAM free while the application failed
A large, mostly empty frame labelled RAM 40 GB; inside it a small frame labelled JVM 1 GB packed with black blocks, and four orange blocks that spill over its top edge

A web application on Adobe ColdFusion with a local MariaDB database and a report export that strains memory

Challenge

The application worked correctly at low traffic. At peak hours the response time grew, some requests ended with an error 500, and in the worst cases the server needed a restart. The symptoms looked like an overloaded application server, but the CPU use and the free RAM of the system did not explain the failures.

The task: find out why the application loses stability under load, remove the cause without an unjustified expansion of the infrastructure, and prove by measurement that the change fixes the problem. Two different causes have the same symptom, a heap that is too small and a memory leak, and they need different fixes.

Solution

We reproduced the problem on a real database: a MariaDB with a catalogue of 40 000 products and a ColdFusion endpoint that reads fifteen thousand rows and builds a report from them, held in memory for the duration of the request. A realistic export or report pattern, about 68 MB of heap per request.

A concurrency ramp from 1 to 20 requests found the exact point at which the heap runs out, confirmed by the ColdFusion logs. A check after the load ruled out a leak: the memory returned to its baseline, and the live set after a forced garbage collection was tiny. Then the JVM configuration was corrected to match the real production configuration, a larger heap limit, limits for Metaspace and the code cache, and a heap dump on an out-of-memory error, and the identical load test was repeated.

Results

Error threshold before the fix
First OutOfMemoryError at 14 concurrent requests, with a 1 GB heap
Before the fix at 20 concurrent requests
10 of 20 requests ended with an error 500
After the fix
1 to 20 concurrent requests: every response 200, zero errors, with a 20 GB heap
Peak heap use after the fix
About 4.8 GB of 18.2 GB available
Live set after a forced garbage collection
About 140 MB, so no leak
Free RAM of the server during the failures
About 30 GB of 40 GB, so the heap was the problem, not the RAM

How we worked

  1. Reproduce on a real database

    A MariaDB with 40 000 products and a report endpoint that reads 15 000 rows and holds the result in memory for the duration of the request, about 68 MB of heap each.

  2. Measure the threshold

    A ramp from 1 to 20 concurrent requests, recording HTTP codes, heap use and every OutOfMemoryError in the ColdFusion logs.

  3. Rule out a leak

    After the load the heap returned to its baseline, and the live set after a forced garbage collection was about 140 MB. A larger heap therefore fixes the cause instead of postponing it.

  4. Correct the JVM configuration

    The values of the real production configuration: a larger heap limit, limits for Metaspace and the code cache, and a heap dump on an out-of-memory error on a volume with room for it.

  5. Repeat the identical test

    The same ramp after the restart: every request from 1 to 20 concurrent answered 200, the dump directory stayed empty.

  6. Leave the diagnostics on

    The next out-of-memory error, if it ever comes, leaves a dump for analysis. The directory is protected, because a dump can contain application data.

Challenges we overcame

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.

A bar chart of concurrent requests from 2 to 20: solid black bars up to 12, from 14 on orange outlined bars with an orange cross above each, and a dashed threshold line between 12 and 14

The same load test before and after

Concurrent requests Before the fix, 1 GB heap After the fix, 20 GB heap
1 to 12 All responses 200 All responses 200
14 First OutOfMemoryError, 4 errors All responses 200
16 6 errors All responses 200
18 8 errors All responses 200
20 10 errors All responses 200, peak heap about 4.8 GB

What we take from it

  • Monitor the JVM heap, not only the RAM of the server. Free system memory and free heap are different figures; an application can fail with 30 GB of RAM free.
  • Alert on both variants of the error. GC overhead limit exceeded is the earlier signal, Java heap space the later one.
  • Do not mistake request throttling for a fix. If the error occurs below the limit of concurrent requests, raising that limit makes it worse.
  • Tell a small heap from a leak. Check whether the memory returns after the load and what the live set is after a forced garbage collection. A larger heap cures a small heap, not a leak.
  • Size the heap from measurements and from the RAM. Neither too small, which means errors, nor too large, which starves the system and lengthens the pauses of a full collection. 20 GB was safe on a server with 40 GB; on a smaller one it would be a mistake.
  • Prepare the diagnostics before they are needed. Enable the heap dump, and point it at a directory with real spare capacity.
  • Prove by measurement. The same load test before and after turns "it seems to work" into a number.

Sources

Two heaps side by side: a narrow one labelled 1 GB, full to the top with orange blocks spilling over its edge, and a wide one labelled 20 GB that is a quarter full with a dashed line at the fill level

JVM settings before and after (as of 2026-10)

Setting Before After
Minimum heap size, -Xms 256 MB 256 MB
Maximum heap size, -Xmx 1 024 MB 20 480 MB
-XX:MaxMetaspaceSize Not set 2 560 MB
-XX:ReservedCodeCacheSize Not set 256 MB
Diagnostics None -XX:+HeapDumpOnOutOfMemoryError with a dump path on a volume with spare capacity
Environment Adobe ColdFusion 2025, Java 21 with the parallel garbage collector, MariaDB 10.11

Related situations

  • Your developer has resigned or already left, and the system has to keep running. We review the system and give you a written report 48 hours after access: its state, the immediate risks and a plan for the first 30 days.

  • According to Adobe, ColdFusion 2021 has received no security patches since 10 November 2025. We compare four routes: stay and harden, update, move to Lucee or replace the system step by step, with dates and risks (As of 2026-10).

More projects

  • A score scale with a dashed threshold: the phishing envelope moves in an orange arc across the threshold

    A spam filter that ran but did not protect

    Services up, configuration valid, no errors in the system status. Yet phishing landed in the inboxes while partners' newsletters went to spam. Our engineers measured which detection mechanisms actually fired, repaired the causes, and three months later found and fixed a regression that only showed up hour by hour.

  • Four envelopes pass the gates SPF, DKIM and DMARC, each ticked in orange, on the way into the inbox

    Email deliverability for four company domains

    A mail server for four company domains signed nothing, published no DMARC policy and was rejected by Microsoft. Our engineers audited it, piloted the change on one domain and rolled it out in batches. The result: all three checks pass at Gmail, the Microsoft block was lifted, and the users noticed nothing.

In 20 minutes we work out which first step fits your system

You tell us the goal and the current state, and we say openly whether and how we can help. Reply within one working day, no obligation.

Phone (German, Polish, English) +48 501 250 220
Email Email

Reachable: Monday to Friday, 9:00 to 17:00 Central European Time; support and emergencies around the clock (24/7)