From 90c48f20e08478969eee2cebd754ba4104c5b56c Mon Sep 17 00:00:00 2001 From: Dhananjay Mahtha Date: Sun, 21 Dec 2025 16:53:25 +0530 Subject: [PATCH] Fix: Prevent cache memory leak by adding maxKeys limit and conditional caching - Add maxKeys limit (10,000) to NodeCache to prevent unbounded memory growth - Skip caching undefined values when GeoIP/ASN lookups fail (e.g., when MaxMind DB not configured) - Add periodic cache statistics logging every 5 minutes for monitoring - Fixes memory leak where cache would grow indefinitely with high request volumes The maxKeys limit uses LRU eviction, so oldest entries are automatically removed when the limit is reached. With ~10k requests/day and 5min TTL, 10k keys provides ample headroom while preventing OOM issues. Fixes #2120 --- server/lib/cache.ts | 17 ++++++++++++++++- server/private/lib/logAccessAudit.ts | 7 +++++-- server/routers/badger/verifySession.ts | 7 +++++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/server/lib/cache.ts b/server/lib/cache.ts index 82c80280..4910d945 100644 --- a/server/lib/cache.ts +++ b/server/lib/cache.ts @@ -1,5 +1,20 @@ import NodeCache from "node-cache"; +import logger from "@server/logger"; -export const cache = new NodeCache({ stdTTL: 3600, checkperiod: 120 }); +// Create cache with maxKeys limit to prevent memory leaks +// With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient +export const cache = new NodeCache({ + stdTTL: 3600, + checkperiod: 120, + maxKeys: 10000 +}); + +// Log cache statistics periodically for monitoring +setInterval(() => { + const stats = cache.getStats(); + logger.debug( + `Cache stats - Keys: ${stats.keys}, Hits: ${stats.hits}, Misses: ${stats.misses}, Hit rate: ${stats.hits > 0 ? ((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(2) : 0}%` + ); +}, 300000); // Every 5 minutes export default cache; diff --git a/server/private/lib/logAccessAudit.ts b/server/private/lib/logAccessAudit.ts index 98eaa6ec..5c423c60 100644 --- a/server/private/lib/logAccessAudit.ts +++ b/server/private/lib/logAccessAudit.ts @@ -161,8 +161,11 @@ async function getCountryCodeFromIp(ip: string): Promise { if (!cachedCountryCode) { cachedCountryCode = await getCountryCodeForIp(ip); // do it locally - // Cache for longer since IP geolocation doesn't change frequently - cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes + // Only cache successful lookups to avoid filling cache with undefined values + if (cachedCountryCode) { + // Cache for longer since IP geolocation doesn't change frequently + cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes + } } return cachedCountryCode; diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index e209ee57..45f5f041 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -1206,8 +1206,11 @@ async function getCountryCodeFromIp(ip: string): Promise { if (!cachedCountryCode) { cachedCountryCode = await getCountryCodeForIp(ip); // do it locally - // Cache for longer since IP geolocation doesn't change frequently - cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes + // Only cache successful lookups to avoid filling cache with undefined values + if (cachedCountryCode) { + // Cache for longer since IP geolocation doesn't change frequently + cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes + } } return cachedCountryCode;