Handling NaN Values in Java Micrometer Gauges with Prometheus
Micrometer is the de facto metrics instrumentation library for JVM-based applications, and Prometheus is a common backend for collecting and visualizing metrics. While Micrometer generally works seamlessly, a frequent issue we might encounter is NaN (Not a Number) values appearing in gauges. This article explains why NaN values occur, and provides strategies to prevent and fix them.
1. Understanding Gauges in Micrometer
A gauge in Micrometer represents a value that can increase or decrease over time, such as queue size, memory usage, active thread count, or cache entries. Unlike counters, gauges are evaluated at scrape time; Micrometer does not store the value internally but instead reads it from an underlying object or function whenever Prometheus scrapes the /metrics endpoint.
1.1 Common Cause of NaN Values in Micrometer Gauges
Micrometer gauges are evaluated lazily at scrape time. To avoid memory leaks, Micrometer intentionally does not hold strong references to the objects being observed by a gauge. As a result, it becomes the application’s responsibility to ensure that the underlying state object remains reachable for the lifetime of the metric.
If the object backing a gauge is dereferenced and later garbage-collected, Micrometer can no longer read its value. When this happens, the gauge may begin reporting a NaN value or disappear entirely from the metrics endpoint, depending on the registry implementation.
This issue typically presents itself as a gauge that behaves correctly immediately after startup, reports valid values for a short period, and then suddenly starts returning NaN or stops appearing in /actuator/metrics. When you see this pattern, it almost always indicates that the object being measured by the gauge has been garbage-collected.
To understand this more clearly, consider the following example.
2. Project Setup
To reproduce and fix the issue, start with a basic Spring Boot project. Add the following dependencies to your pom.xml.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <scope>runtime</scope> </dependency>
3. Example: Gauge Reporting NaN Due to Garbage Collection
To illustrate how a NaN value can occur, we create a service class and register a gauge that observes an object.
@Service
public class SampleMetricsService {
public SampleMetricsService(MeterRegistry meterRegistry) {
AtomicInteger sampleValue = new AtomicInteger(10);
// Register gauge observing the AtomicInteger
Gauge.builder("sample.value", sampleValue, AtomicInteger::get)
.description("Sample gauge that may produce NaN")
.register(meterRegistry);
}
}
At first glance, this code appears correct. The gauge is registered successfully and initially reports a value. However, sampleValue is a method-local variable. Once the constructor completes, there are no strong references to it, making it eligible for garbage collection.
After the object is collected, Micrometer can no longer read its state, and the gauge will eventually report a NaN value.
Observing the Issue
Start the Spring Boot application locally and then perform a GET request to the actuator endpoint to retrieve the exposed metrics.
curl http://localhost:8080/actuator/metrics/sample.value
You may immediately or eventually see a value of NaN in the metrics output:
{
"name": "sample.value",
"description": "Sample gauge that may produce NaN",
"measurements": [
{
"statistic": "VALUE",
"value": "NaN"
}
],
"availableTags": []
}
NaN occurs in this code because the object being observed by the Micrometer gauge does not have a stable lifecycle. The AtomicInteger sampleValue is declared as a method-local variable inside the constructor, which means it exists only during the execution of the constructor. Once the constructor finishes, there are no remaining strong references to this object in the application.
Micrometer deliberately avoids holding strong references to objects registered with gauges in order to prevent memory leaks. As a result, the AtomicInteger referenced by the gauge is eligible for garbage collection shortly after the service is created. When the JVM garbage collector reclaims this object, Micrometer can no longer read its value.
Because gauges are evaluated lazily at scrape time, Micrometer attempts to read the gauge value only when Prometheus (or the Actuator metrics endpoint) requests it. If the underlying object has already been garbage-collected at that moment, Micrometer cannot retrieve a numeric value and returns NaN instead. In short, the NaN value is not caused by the gauge registration itself but by the loss of a strong reference to the object being measured.
4. Fix: Maintain a Strong Reference to the Gauged Object
Here are some valid ways to fix the NaN issue caused by garbage collection. Both approaches ensure that the object backing the gauge remains strongly referenced so Micrometer can reliably sample its value.
Force a Strong Reference in the Gauge
@Service
public class SampleMetricsService {
public SampleMetricsService(MeterRegistry meterRegistry) {
AtomicInteger sampleValue = new AtomicInteger(10);
Gauge.builder("sample.value", sampleValue, AtomicInteger::get)
.description("Sample gauge that may produce NaN")
.strongReference(true)
.register(meterRegistry);
}
}
In this approach, the call to strongReference(true) instructs Micrometer to retain a strong reference to the AtomicInteger. This prevents the object from being garbage-collected even though it is declared as a local variable. As a result, the gauge continues to report valid values instead of returning NaN.
Keep the Gauged Object as a Long-Lived Field
@Service
public class SampleMetricsService {
private final AtomicInteger sampleValue = new AtomicInteger(10);
public SampleMetricsService(MeterRegistry meterRegistry) {
Gauge.builder("sample.value", sampleValue, AtomicInteger::get)
.description("Sample gauge that may produce NaN")
.register(meterRegistry);
}
}
By storing the AtomicInteger as a class-level field, the object remains strongly referenced for the lifetime of the service. Micrometer can consistently read the value at scrape time without relying on forced strong references.
Testing the Fix
Start the application and call the actuator endpoint again.
{
"name": "sample.value",
"description": "Sample gauge that may produce NaN",
"measurements": [
{
"statistic": "VALUE",
"value": 10
}
],
"availableTags": []
}
You should now see a valid numeric value instead of NaN. Both fixes prevent NaN values by ensuring the gauged object is not garbage-collected.
5. Conclusion
In this article, we explored why NaN values occur in Java Micrometer gauge metrics, demonstrated how the issue can be reproduced, and showed practical fixes to ensure reliable Prometheus monitoring.
6. Download the Source Code
This was an article on resolving a NaN value in a Java Micrometer gauge with Prometheus monitoring.
You can download the full source code of this example here: Java prometheus micrometer gauge NaN value




