Problem
siloQueryCache is bounded by entry count, not memory:
spring.cache.caffeine.spec=maximumSize=50000,softValues
One entry can be a List of millions of AggregationData, each holding a
Map<String, JsonNode> with hundreds of entries, so 50 000 entries can be many GB.
softValues is the only backstop, and it only clears entries once the JVM is already
near OOM - too late under a burst, and soft references add GC pressure. This contributed
to a heap exhaustion / GC-thrash outage on 2026-09-01.
Proposal
Bound the cache by the estimated retained heap size of its entries instead of by entry
count:
-
Configure Caffeine programmatically (a Caffeine bean, which Spring Boot uses in
preference to spring.cache.caffeine.spec) with maximumWeight + a Weigher.
maximumWeight and maximumSize are mutually exclusive, so this replaces
maximumSize; drop softValues as well.
-
A weigher that estimates bytes per cached result - per row, plus per group-by column
for aggregations, since an aggregation row is a Map whose cost scales with the number
of columns. Rough sketch:
private const val ROW_OVERHEAD_BYTES = 64L
private const val AGGREGATION_FIELD_BYTES = 120L
private const val NON_AGGREGATION_ROW_BYTES = 200L
class SiloQueryCacheWeigher : Weigher<Any, Any> {
override fun weigh(key: Any, value: Any): Int {
val rows = (value as? WithDataVersion<*>)?.queryResult as? List<*> ?: return 1
var weight = 0L
for (row in rows) {
weight += when (row) {
is AggregationData -> ROW_OVERHEAD_BYTES + row.fields.size * AGGREGATION_FIELD_BYTES
else -> NON_AGGREGATION_ROW_BYTES
}
if (weight >= Int.MAX_VALUE) return Int.MAX_VALUE
}
return weight.toInt().coerceAtLeast(1)
}
}
-
Default the limit to a fraction of the max heap (Runtime.maxMemory(), effectively
-Xmx) so it scales with the instance, e.g. 0.25. Allow overrides via
lapis.cache.siloQueryMaxWeightHeapFraction and an absolute
lapis.cache.siloQueryMaxWeightBytes (taking precedence when set).
-
Enable recordStats() so hit rate and evictions show up in metrics (currently only
cache.size is exposed - the cache logs a warning that it records no statistics).
Blocking (sort of)
We need to have sound values for row overhead and cell value bytes, otherwise the weigher will be off and we can still run into OOM issues. At the time of writing I didn't feel confident in calculating or guessing these values, and it would probably be best to measure this.
Problem
siloQueryCacheis bounded by entry count, not memory:One entry can be a
Listof millions ofAggregationData, each holding aMap<String, JsonNode>with hundreds of entries, so 50 000 entries can be many GB.softValuesis the only backstop, and it only clears entries once the JVM is alreadynear OOM - too late under a burst, and soft references add GC pressure. This contributed
to a heap exhaustion / GC-thrash outage on 2026-09-01.
Proposal
Bound the cache by the estimated retained heap size of its entries instead of by entry
count:
Configure Caffeine programmatically (a
Caffeinebean, which Spring Boot uses inpreference to
spring.cache.caffeine.spec) withmaximumWeight+ aWeigher.maximumWeightandmaximumSizeare mutually exclusive, so this replacesmaximumSize; dropsoftValuesas well.A weigher that estimates bytes per cached result - per row, plus per group-by column
for aggregations, since an aggregation row is a
Mapwhose cost scales with the numberof columns. Rough sketch:
Default the limit to a fraction of the max heap (
Runtime.maxMemory(), effectively-Xmx) so it scales with the instance, e.g.0.25. Allow overrides vialapis.cache.siloQueryMaxWeightHeapFractionand an absolutelapis.cache.siloQueryMaxWeightBytes(taking precedence when set).Enable
recordStats()so hit rate and evictions show up in metrics (currently onlycache.sizeis exposed - the cache logs a warning that it records no statistics).Blocking (sort of)
We need to have sound values for row overhead and cell value bytes, otherwise the weigher will be off and we can still run into OOM issues. At the time of writing I didn't feel confident in calculating or guessing these values, and it would probably be best to measure this.