Project Overview
The project is a personal high-throughput e-commerce storefront API built with Next.js Server-Side Rendering (SSR) and Redis for distributed caching. The system was designed to handle high-concurrency traffic during simulated flash sales. However, as the traffic volume scaled, the application suffered from severe systemic degradation: recurrent Out-Of-Memory (OOM) crashes and catastrophic latency spikes causing HTTP 502 Bad Gateway errors. Stability in memory allocation and CPU overhead is critical for the application's survival in cloud-native environments.
Bug Fix or Performance Improvement
I identified two deeply intertwined architectural anti-patterns that created a cascading failure:
- V8 Old Generation Memory Leak: In SSR environments, module-level state persists across requests. The code unintentionally held closures over massive HTTP
reqobjects inside a global metrics array. Because these objects maintained active references, the V8 JavaScript Engine's Mark and Sweep algorithm could not reclaim them. They survived multiple garbage collection cycles, eventually filling up the Old Generation memory space. - Redis Cache Stampede (Thundering Herd): At the exact millisecond a highly requested Redis cache entry reached its Time-To-Live (TTL), thousands of concurrent SSR requests experienced a cache miss. This triggered a massive, synchronized assault on the backend PostgreSQL database, completely exhausting the connection pool.
The objective was to completely decouple the memory closures and implement a probabilistic mathematical model to prevent the stampede without relying on expensive distributed mutex locks.
Code
Code Changes: This optimization was implemented in a private personal project repository. The exact code changes are demonstrated in the snippets below.
Before: The Leaky Closure & Stampede Trigger
// BUG 1: Module-level state persisting across SSR requests
const globalMetricsCache = [];
export async function fetchProductDataSSR(req, productId) {
// FATAL: Storing the huge 'req' object in a global array prevents V8 Garbage Collection.
globalMetricsCache.push({ timestamp: Date.now(), req });
const cacheKey = `product:${productId}`;
const cached = await redis.get(cacheKey);
// BUG 2: Cache Stampede. When 'cached' is null, 10,000 requests
// hit the database simultaneously.
if (cached) return JSON.parse(cached);
const data = await db.query('SELECT * FROM products WHERE id = ?', [productId]);
await redis.set(cacheKey, JSON.stringify(data), 'EX', 60);
return data;
}
After: V8 GC Fix & XFetch Algorithm Integration
// FIX 1: Removing module-level state and decoupling 'req' closure.
export async function fetchProductDataSSR(productId) {
const cacheKey = `product:${productId}`;
const cachedItem = await redis.get(cacheKey);
if (cachedItem) {
const { data, delta, expiryAt } = JSON.parse(cachedItem);
const now = Date.now();
// FIX 2: Probabilistic Early Expiration (XFetch)
// Distributes expiration times to reduce the likelihood of a cache stampede.
const beta = 1.0;
const randomLog = -Math.log(Math.random());
// If the cache is nearing expiration, probabilistically volunteer ONE request
// to recompute the data in the background, serving stale data immediately.
if (now - (delta * beta * randomLog) >= expiryAt) {
recomputeAndCache(productId, cacheKey);
}
return data;
}
return await recomputeAndCache(productId, cacheKey);
}
async function recomputeAndCache(productId, cacheKey) {
const start = Date.now();
const data = await db.query('SELECT * FROM products WHERE id = ?', [productId]);
const delta = Date.now() - start;
// Set actual TTL significantly higher than XFetch probabilistic boundary
const payload = JSON.stringify({
data,
delta, // Recomputation time
expiryAt: Date.now() + 60000 // 60s soft-expiry
});
await redis.set(cacheKey, payload, 'EX', 120); // 120s hard-expiry
return data;
}
My Improvements
The optimization bridges advanced system architecture with computer science fundamentals:
-
Memory Heap Stabilization: By eliminating the
globalMetricsCachethat trapped thereqclosures, the V8 Garbage Collector can finally sweep the orphaned objects. Memory usage fluctuations became stable, removing the continuous upward trend that signifies a memory leak. - Implementing Probabilistic Early Expiration: To solve the stampede, I applied the XFetch algorithm. This technique involves distributing the expiration times by adding a randomized element.
-
The Mathematics: The formula
now - (delta * beta * -log(rand)) >= expiryAtevaluates whether the current time is perilously close to the expiration. It multiplies the recomputation latency (delta) by a random exponential distribution (-log(rand)). This guarantees that exactly one concurrent process probabilistically decides to refresh the cache in the background right before the 60s mark, while the rest are served the existing cache. The synchronized 60-second database miss vanished completely.
Best Use of Sentry
I integrated @sentry/profiling-node to leverage Continuous Profiling. Continuous profiling allows for the collection of runtime performance data—including CPU usage and garbage collection overhead—without interrupting running applications.
- Identifying GC Overhead: Traditional APMs missed the issue, but Sentry's continuous profiling identified unnecessary garbage collection cycles. The V8 Engine was spending over 60% of its CPU time in a "Mark and Sweep" death spiral, desperately trying to free up the Old Generation memory that was locked by my closures.
- Trace Correlation: Sentry's flame graphs perfectly correlated the HTTP latency spikes with the exact timestamps of the Redis cache expiration, proving the existence of the Cache Stampede. Post-deployment, the continuous profiling flame graphs showed a perfectly flat database span distribution.
Best Use of Google AI
To move beyond basic Mutex locking, I utilized Google Gemini. I fed Gemini a V8 Heap Snapshot and Sentry's flame graphs. Gemini's context window analyzed the heap and pinpointed the Node.js closure leak pattern.
Furthermore, when I asked Gemini for an academic, lock-free approach to cache stampedes, it summarized the concepts from the "Optimal Probabilistic Cache Stampede Prevention" research paper and helped generate the exact non-blocking Node.js mathematical implementation for the XFetch formula, ensuring the Math.random() distribution would not block the event loop.
Top comments (0)