Node.js Memory Optimization in Containers: Production Performance Guide
Master Node.js memory optimization for containerized applications in Kubernetes. Learn V8 engine tuning, memory leak detection and prevention, garbage collection optimization, and production-ready monitoring strategies for high-performance Node.js deployments.
Node.js Memory Optimization in Containers: Production Performance Guide
Executive Summary
Node.js applications in containerized environments face unique memory management challenges. The V8 JavaScript engine’s default heap size limits, combined with container memory constraints, can lead to out-of-memory errors, performance degradation, and application crashes. This comprehensive guide covers production-proven techniques for optimizing Node.js memory usage in Kubernetes, including V8 heap tuning, memory leak detection, garbage collection optimization, and enterprise-grade monitoring strategies.
Understanding Node.js Memory Architecture
V8 Memory Model
Memory Components
// Node.js memory breakdown
const v8 = require('v8');
const process = require('process');
function displayMemoryUsage() {
const heapStats = v8.getHeapStatistics();
const memUsage = process.memoryUsage();
console.log('=== V8 Heap Statistics ===');
console.log(`Total Heap Size: ${(heapStats.total_heap_size / 1024 / 1024).toFixed(2)} MB`);
console.log(`Used Heap Size: ${(heapStats.used_heap_size / 1024 / 1024).toFixed(2)} MB`);
console.log(`Heap Size Limit: ${(heapStats.heap_size_limit / 1024 / 1024).toFixed(2)} MB`);
console.log(`Available Size: ${(heapStats.total_available_size / 1024 / 1024).toFixed(2)} MB`);
console.log('\n=== Process Memory Usage ===');
console.log(`RSS (Resident Set Size): ${(memUsage.rss / 1024 / 1024).toFixed(2)} MB`);
console.log(`Heap Used: ${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`);
console.log(`Heap Total: ${(memUsage.heapTotal / 1024 / 1024).toFixed(2)} MB`);
console.log(`External: ${(memUsage.external / 1024 / 1024).toFixed(2)} MB`);
console.log(`Array Buffers: ${(memUsage.arrayBuffers / 1024 / 1024).toFixed(2)} MB`);
}
// Monitor memory usage
setInterval(displayMemoryUsage, 60000); // Every minute
Memory Allocation Model
# Node.js Container Memory Breakdown (4GB container)
Total Container Memory: 4096 MB
├── V8 Heap (New + Old Space): 2048 MB (--max-old-space-size)
│ ├── New Space (Young Generation): 64 MB (--max-semi-space-size)
│ └── Old Space: 1984 MB
├── Code Space: 512 MB (compiled JavaScript)
├── Map Space: 128 MB (hidden classes, maps)
├── Large Object Space: 256 MB (objects > 512KB)
├── External Memory: 512 MB (Buffers, native objects)
├── C++ Objects: 256 MB (Node.js core, addons)
└── OS Overhead: 384 MB (process metadata, stacks)
Default Memory Limits
V8 Default Heap Sizes
# Default heap limits by system architecture
# 32-bit: ~512 MB
# 64-bit: ~1.4 GB (Node.js < 16)
# 64-bit: ~2 GB (Node.js 16+)
# Check current limits
node -e "console.log(require('v8').getHeapStatistics().heap_size_limit / 1024 / 1024 + ' MB')"
Production Memory Configuration
Container-Optimized Node.js Configuration
Dockerfile with Memory Tuning
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --only=production && \
npm cache clean --force
COPY . .
# Build if needed (TypeScript, etc.)
RUN npm run build
FROM node:20-alpine
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# Copy application
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
USER nodejs
# Environment variables for memory tuning
ENV NODE_ENV=production \
NODE_OPTIONS="--max-old-space-size=3072 --max-semi-space-size=64"
EXPOSE 3000
# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]
Kubernetes Deployment Configuration
Production-Ready Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: nodejs-application
namespace: production
labels:
app: nodejs-application
version: v1.0.0
spec:
replicas: 3
selector:
matchLabels:
app: nodejs-application
template:
metadata:
labels:
app: nodejs-application
version: v1.0.0
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
containers:
- name: application
image: company/nodejs-application:1.0.0
env:
# Memory Configuration
- name: NODE_ENV
value: "production"
- name: NODE_OPTIONS
value: "--max-old-space-size=3072 --max-semi-space-size=64 --expose-gc"
# V8 Optimization Flags
- name: UV_THREADPOOL_SIZE
value: "8" # Default is 4, increase for I/O heavy apps
# Application Configuration
- name: PORT
value: "3000"
- name: METRICS_PORT
value: "9090"
ports:
- containerPort: 3000
name: http
protocol: TCP
- containerPort: 9090
name: metrics
protocol: TCP
resources:
requests:
memory: "4Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
terminationGracePeriodSeconds: 30
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- nodejs-application
topologyKey: kubernetes.io/hostname
Memory Leak Detection and Prevention
Heap Snapshot Analysis
Automated Heap Snapshot Capture
// memory-profiler.js
const v8 = require('v8');
const fs = require('fs');
const path = require('path');
class MemoryProfiler {
constructor(options = {}) {
this.threshold = options.threshold || 0.85; // 85% memory usage
this.snapshotDir = options.snapshotDir || '/var/log/heapdumps';
this.checkInterval = options.checkInterval || 30000; // 30 seconds
this.maxSnapshots = options.maxSnapshots || 5;
this.ensureSnapshotDir();
this.startMonitoring();
}
ensureSnapshotDir() {
if (!fs.existsSync(this.snapshotDir)) {
fs.mkdirSync(this.snapshotDir, { recursive: true });
}
}
getMemoryUsagePercent() {
const heapStats = v8.getHeapStatistics();
return heapStats.used_heap_size / heapStats.heap_size_limit;
}
takeHeapSnapshot() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `heap-${timestamp}.heapsnapshot`;
const filepath = path.join(this.snapshotDir, filename);
console.log(`[MemoryProfiler] Taking heap snapshot: ${filename}`);
const writeStream = fs.createWriteStream(filepath);
const snapshotStream = v8.writeHeapSnapshot();
fs.createReadStream(snapshotStream).pipe(writeStream);
writeStream.on('finish', () => {
console.log(`[MemoryProfiler] Heap snapshot saved: ${filepath}`);
this.cleanupOldSnapshots();
});
return filepath;
}
cleanupOldSnapshots() {
const files = fs.readdirSync(this.snapshotDir)
.filter(f => f.endsWith('.heapsnapshot'))
.map(f => ({
name: f,
path: path.join(this.snapshotDir, f),
time: fs.statSync(path.join(this.snapshotDir, f)).mtime.getTime()
}))
.sort((a, b) => b.time - a.time);
// Keep only the most recent snapshots
const toDelete = files.slice(this.maxSnapshots);
toDelete.forEach(file => {
fs.unlinkSync(file.path);
console.log(`[MemoryProfiler] Deleted old snapshot: ${file.name}`);
});
}
checkMemoryUsage() {
const usage = this.getMemoryUsagePercent();
const memUsage = process.memoryUsage();
console.log(`[MemoryProfiler] Memory usage: ${(usage * 100).toFixed(2)}%`);
console.log(` Heap: ${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB / ${(memUsage.heapTotal / 1024 / 1024).toFixed(2)} MB`);
console.log(` RSS: ${(memUsage.rss / 1024 / 1024).toFixed(2)} MB`);
if (usage >= this.threshold) {
console.warn(`[MemoryProfiler] Memory threshold exceeded: ${(usage * 100).toFixed(2)}%`);
this.takeHeapSnapshot();
// Force garbage collection if enabled
if (global.gc) {
console.log('[MemoryProfiler] Forcing garbage collection');
global.gc();
}
}
}
startMonitoring() {
console.log(`[MemoryProfiler] Started monitoring with ${(this.threshold * 100)}% threshold`);
setInterval(() => this.checkMemoryUsage(), this.checkInterval);
}
}
module.exports = MemoryProfiler;
// Usage
if (require.main === module) {
const profiler = new MemoryProfiler({
threshold: 0.85,
checkInterval: 30000,
maxSnapshots: 5
});
}
Memory Leak Detection Patterns
Common Memory Leak Patterns and Solutions
// memory-leak-patterns.js
// PROBLEM: Global variables accumulating data
class LeakyCache {
constructor() {
this.cache = {}; // Never cleared!
}
set(key, value) {
this.cache[key] = value;
}
get(key) {
return this.cache[key];
}
}
// SOLUTION: Bounded cache with TTL
class BoundedCache {
constructor(maxSize = 1000, ttl = 3600000) {
this.cache = new Map();
this.maxSize = maxSize;
this.ttl = ttl;
}
set(key, value) {
// Evict oldest if at capacity
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, {
value,
timestamp: Date.now()
});
}
get(key) {
const entry = this.cache.get(key);
if (!entry) return null;
// Check TTL
if (Date.now() - entry.timestamp > this.ttl) {
this.cache.delete(key);
return null;
}
return entry.value;
}
clear() {
this.cache.clear();
}
}
// PROBLEM: Event listeners not removed
class LeakyEventHandler {
constructor(eventEmitter) {
this.handler = (data) => this.process(data);
eventEmitter.on('data', this.handler); // Never removed!
}
process(data) {
console.log(data);
}
}
// SOLUTION: Proper cleanup
class ProperEventHandler {
constructor(eventEmitter) {
this.eventEmitter = eventEmitter;
this.handler = (data) => this.process(data);
this.eventEmitter.on('data', this.handler);
}
process(data) {
console.log(data);
}
cleanup() {
this.eventEmitter.removeListener('data', this.handler);
}
}
// PROBLEM: Closures capturing large objects
class LeakyClosure {
processLargeData(largeObject) {
return setInterval(() => {
console.log(largeObject.id); // Entire object kept in memory!
}, 1000);
}
}
// SOLUTION: Capture only what you need
class OptimizedClosure {
processLargeData(largeObject) {
const id = largeObject.id; // Copy only needed data
return setInterval(() => {
console.log(id);
}, 1000);
}
}
// PROBLEM: Promises not resolved
class LeakyPromise {
async fetchData() {
return new Promise((resolve, reject) => {
// If this never resolves, the promise and its context stay in memory
setTimeout(() => {
// Oops, forgot to resolve or reject!
}, 1000);
});
}
}
// SOLUTION: Always resolve/reject with timeout
class SafePromise {
async fetchData(timeout = 5000) {
return Promise.race([
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('data');
}, 1000);
}),
new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Timeout'));
}, timeout);
})
]);
}
}
module.exports = {
BoundedCache,
ProperEventHandler,
OptimizedClosure,
SafePromise
};
Production Memory Monitoring
Comprehensive Monitoring Service
// monitoring.js
const promClient = require('prom-client');
const express = require('express');
const v8 = require('v8');
class MemoryMonitoringService {
constructor(port = 9090) {
this.port = port;
this.register = new promClient.Registry();
// Enable default metrics
promClient.collectDefaultMetrics({
register: this.register,
prefix: 'nodejs_'
});
this.setupCustomMetrics();
this.setupServer();
this.startMonitoring();
}
setupCustomMetrics() {
// Heap usage metrics
this.heapUsedGauge = new promClient.Gauge({
name: 'nodejs_heap_used_bytes',
help: 'V8 heap used size in bytes',
registers: [this.register]
});
this.heapTotalGauge = new promClient.Gauge({
name: 'nodejs_heap_total_bytes',
help: 'V8 heap total size in bytes',
registers: [this.register]
});
this.heapLimitGauge = new promClient.Gauge({
name: 'nodejs_heap_limit_bytes',
help: 'V8 heap size limit in bytes',
registers: [this.register]
});
// Memory usage metrics
this.rssGauge = new promClient.Gauge({
name: 'nodejs_rss_bytes',
help: 'Resident set size in bytes',
registers: [this.register]
});
this.externalGauge = new promClient.Gauge({
name: 'nodejs_external_bytes',
help: 'External memory usage in bytes',
registers: [this.register]
});
// Garbage collection metrics
this.gcDurationHistogram = new promClient.Histogram({
name: 'nodejs_gc_duration_seconds',
help: 'Garbage collection duration in seconds',
labelNames: ['kind'],
buckets: [0.001, 0.01, 0.1, 1, 2, 5],
registers: [this.register]
});
this.gcCounter = new promClient.Counter({
name: 'nodejs_gc_count_total',
help: 'Total garbage collection count',
labelNames: ['kind'],
registers: [this.register]
});
// Event loop lag
this.eventLoopLagGauge = new promClient.Gauge({
name: 'nodejs_eventloop_lag_seconds',
help: 'Event loop lag in seconds',
registers: [this.register]
});
// Active handles and requests
this.activeHandlesGauge = new promClient.Gauge({
name: 'nodejs_active_handles',
help: 'Number of active handles',
registers: [this.register]
});
this.activeRequestsGauge = new promClient.Gauge({
name: 'nodejs_active_requests',
help: 'Number of active requests',
registers: [this.register]
});
}
setupServer() {
const app = express();
app.get('/metrics', async (req, res) => {
res.set('Content-Type', this.register.contentType);
res.end(await this.register.metrics());
});
app.get('/health/live', (req, res) => {
res.json({ status: 'ok' });
});
app.get('/health/ready', (req, res) => {
const memUsage = process.memoryUsage();
const heapStats = v8.getHeapStatistics();
const heapUsedPercent = heapStats.used_heap_size / heapStats.heap_size_limit;
if (heapUsedPercent > 0.95) {
return res.status(503).json({
status: 'not ready',
reason: 'high memory usage'
});
}
res.json({ status: 'ready' });
});
this.server = app.listen(this.port, () => {
console.log(`[Monitoring] Metrics server listening on port ${this.port}`);
});
}
updateMetrics() {
// Update heap metrics
const heapStats = v8.getHeapStatistics();
this.heapUsedGauge.set(heapStats.used_heap_size);
this.heapTotalGauge.set(heapStats.total_heap_size);
this.heapLimitGauge.set(heapStats.heap_size_limit);
// Update memory usage metrics
const memUsage = process.memoryUsage();
this.rssGauge.set(memUsage.rss);
this.externalGauge.set(memUsage.external);
// Update active handles/requests
const handles = process._getActiveHandles().length;
const requests = process._getActiveRequests().length;
this.activeHandlesGauge.set(handles);
this.activeRequestsGauge.set(requests);
}
measureEventLoopLag() {
const start = Date.now();
setImmediate(() => {
const lag = (Date.now() - start) / 1000;
this.eventLoopLagGauge.set(lag);
});
}
monitorGarbageCollection() {
const gcTypes = {
1: 'Scavenge',
2: 'MarkSweepCompact',
4: 'IncrementalMarking',
8: 'ProcessWeakCallbacks',
15: 'All'
};
const obs = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
const kind = gcTypes[entry.detail.kind] || 'Unknown';
this.gcDurationHistogram.observe({ kind }, entry.duration / 1000);
this.gcCounter.inc({ kind });
});
});
obs.observe({ entryTypes: ['gc'] });
}
startMonitoring() {
// Update metrics every 5 seconds
setInterval(() => this.updateMetrics(), 5000);
// Measure event loop lag every second
setInterval(() => this.measureEventLoopLag(), 1000);
// Monitor garbage collection
this.monitorGarbageCollection();
console.log('[Monitoring] Started monitoring services');
}
close() {
if (this.server) {
this.server.close();
}
}
}
module.exports = MemoryMonitoringService;
// Usage
if (require.main === module) {
const monitoring = new MemoryMonitoringService(9090);
}
Garbage Collection Optimization
Understanding V8 Garbage Collection
GC Monitoring Script
// gc-monitor.js
const v8 = require('v8');
const { PerformanceObserver } = require('perf_hooks');
class GCMonitor {
constructor() {
this.gcStats = {
scavenge: { count: 0, totalDuration: 0, maxDuration: 0 },
markSweepCompact: { count: 0, totalDuration: 0, maxDuration: 0 },
incrementalMarking: { count: 0, totalDuration: 0, maxDuration: 0 },
processWeakCallbacks: { count: 0, totalDuration: 0, maxDuration: 0 }
};
this.setupObserver();
}
setupObserver() {
const gcTypes = {
1: 'scavenge',
2: 'markSweepCompact',
4: 'incrementalMarking',
8: 'processWeakCallbacks'
};
const obs = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
const kind = gcTypes[entry.detail.kind];
if (kind && this.gcStats[kind]) {
const stats = this.gcStats[kind];
stats.count++;
stats.totalDuration += entry.duration;
stats.maxDuration = Math.max(stats.maxDuration, entry.duration);
console.log(`[GC] ${kind}: ${entry.duration.toFixed(2)}ms (${entry.detail.flags})`);
}
});
});
obs.observe({ entryTypes: ['gc'], buffered: true });
}
getStatistics() {
const stats = {};
for (const [type, data] of Object.entries(this.gcStats)) {
if (data.count > 0) {
stats[type] = {
count: data.count,
avgDuration: (data.totalDuration / data.count).toFixed(2) + 'ms',
maxDuration: data.maxDuration.toFixed(2) + 'ms',
totalDuration: data.totalDuration.toFixed(2) + 'ms'
};
}
}
return stats;
}
printStatistics() {
console.log('\n=== Garbage Collection Statistics ===');
const stats = this.getStatistics();
for (const [type, data] of Object.entries(stats)) {
console.log(`\n${type}:`);
console.log(` Count: ${data.count}`);
console.log(` Average Duration: ${data.avgDuration}`);
console.log(` Max Duration: ${data.maxDuration}`);
console.log(` Total Duration: ${data.totalDuration}`);
}
}
}
module.exports = GCMonitor;
// Usage
if (require.main === module) {
const monitor = new GCMonitor();
// Print statistics every minute
setInterval(() => monitor.printStatistics(), 60000);
}
GC Tuning Flags
Node.js GC Configuration
#!/bin/bash
# gc-tuning.sh - Different GC configurations for different workloads
# High Throughput Configuration
# For batch processing, data transformation pipelines
NODE_OPTIONS="--max-old-space-size=4096 \
--max-semi-space-size=128 \
--initial-old-space-size=2048"
# Low Latency Configuration
# For real-time APIs, WebSocket servers
NODE_OPTIONS="--max-old-space-size=2048 \
--max-semi-space-size=32 \
--expose-gc \
--gc-interval=100"
# Memory Constrained Configuration
# For containerized environments with limited memory
NODE_OPTIONS="--max-old-space-size=1024 \
--max-semi-space-size=16 \
--optimize-for-size"
# Debug Configuration
# For development and troubleshooting
NODE_OPTIONS="--max-old-space-size=2048 \
--expose-gc \
--trace-gc \
--trace-gc-verbose"
Stream Processing and Memory Efficiency
Efficient Stream Processing
Memory-Efficient File Processing
// efficient-stream-processing.js
const fs = require('fs');
const { pipeline, Transform } = require('stream');
const { promisify } = require('util');
const zlib = require('zlib');
const pipelineAsync = promisify(pipeline);
// BAD: Loading entire file into memory
async function inefficientFileProcessing(inputFile, outputFile) {
const data = await fs.promises.readFile(inputFile, 'utf8');
const processed = data
.split('\n')
.map(line => line.toUpperCase())
.join('\n');
await fs.promises.writeFile(outputFile, processed);
}
// GOOD: Stream-based processing
async function efficientFileProcessing(inputFile, outputFile) {
const transformStream = new Transform({
transform(chunk, encoding, callback) {
const processed = chunk.toString().toUpperCase();
callback(null, processed);
}
});
await pipelineAsync(
fs.createReadStream(inputFile),
transformStream,
zlib.createGzip(),
fs.createWriteStream(outputFile)
);
}
// Advanced streaming with backpressure handling
class ChunkedProcessor extends Transform {
constructor(processFunc, options = {}) {
super({
...options,
highWaterMark: options.highWaterMark || 16 * 1024 // 16KB
});
this.processFunc = processFunc;
this.buffer = '';
}
_transform(chunk, encoding, callback) {
this.buffer += chunk.toString();
const lines = this.buffer.split('\n');
// Keep last incomplete line in buffer
this.buffer = lines.pop();
try {
const processed = lines
.map(this.processFunc)
.join('\n') + '\n';
callback(null, processed);
} catch (error) {
callback(error);
}
}
_flush(callback) {
if (this.buffer) {
try {
const processed = this.processFunc(this.buffer);
callback(null, processed);
} catch (error) {
callback(error);
}
} else {
callback();
}
}
}
// Usage
async function processLargeFile() {
const processor = new ChunkedProcessor(
line => line.trim().toUpperCase(),
{ highWaterMark: 64 * 1024 }
);
await pipelineAsync(
fs.createReadStream('large-input.txt'),
processor,
zlib.createGzip(),
fs.createWriteStream('output.txt.gz')
);
}
module.exports = {
efficientFileProcessing,
ChunkedProcessor,
processLargeFile
};
Production Configuration Examples
Complete Enterprise Setup
Production-Ready Application
// server.js
const express = require('express');
const MemoryMonitoringService = require('./monitoring');
const MemoryProfiler = require('./memory-profiler');
const GCMonitor = require('./gc-monitor');
class ProductionServer {
constructor() {
this.app = express();
this.port = process.env.PORT || 3000;
// Initialize monitoring
this.monitoring = new MemoryMonitoringService(9090);
this.profiler = new MemoryProfiler({
threshold: 0.85,
checkInterval: 30000,
maxSnapshots: 5
});
this.gcMonitor = new GCMonitor();
this.setupMiddleware();
this.setupRoutes();
this.setupErrorHandling();
this.setupGracefulShutdown();
}
setupMiddleware() {
this.app.use(express.json({ limit: '1mb' }));
this.app.use(express.urlencoded({ extended: true, limit: '1mb' }));
// Request size limiting
this.app.use((req, res, next) => {
const contentLength = req.get('content-length');
if (contentLength && parseInt(contentLength) > 10 * 1024 * 1024) {
return res.status(413).json({ error: 'Payload too large' });
}
next();
});
// Memory usage middleware
this.app.use((req, res, next) => {
const memUsage = process.memoryUsage();
const heapUsedPercent = memUsage.heapUsed / memUsage.heapTotal;
if (heapUsedPercent > 0.9) {
return res.status(503).json({
error: 'Service temporarily unavailable',
reason: 'High memory usage'
});
}
next();
});
}
setupRoutes() {
this.app.get('/health/live', (req, res) => {
res.json({ status: 'ok' });
});
this.app.get('/health/ready', (req, res) => {
const memUsage = process.memoryUsage();
const heapUsedPercent = memUsage.heapUsed / memUsage.heapTotal;
if (heapUsedPercent > 0.95) {
return res.status(503).json({
status: 'not ready',
reason: 'high memory usage',
memoryUsage: {
heapUsedPercent: (heapUsedPercent * 100).toFixed(2) + '%',
heapUsed: (memUsage.heapUsed / 1024 / 1024).toFixed(2) + ' MB',
heapTotal: (memUsage.heapTotal / 1024 / 1024).toFixed(2) + ' MB'
}
});
}
res.json({ status: 'ready' });
});
this.app.get('/api/stats', (req, res) => {
const memUsage = process.memoryUsage();
const v8Stats = require('v8').getHeapStatistics();
const gcStats = this.gcMonitor.getStatistics();
res.json({
memory: {
rss: (memUsage.rss / 1024 / 1024).toFixed(2) + ' MB',
heapUsed: (memUsage.heapUsed / 1024 / 1024).toFixed(2) + ' MB',
heapTotal: (memUsage.heapTotal / 1024 / 1024).toFixed(2) + ' MB',
external: (memUsage.external / 1024 / 1024).toFixed(2) + ' MB'
},
v8: {
heapSizeLimit: (v8Stats.heap_size_limit / 1024 / 1024).toFixed(2) + ' MB',
totalAvailable: (v8Stats.total_available_size / 1024 / 1024).toFixed(2) + ' MB'
},
gc: gcStats,
uptime: process.uptime(),
pid: process.pid
});
});
// Force GC endpoint (for testing only)
if (global.gc) {
this.app.post('/api/gc', (req, res) => {
const before = process.memoryUsage();
global.gc();
const after = process.memoryUsage();
res.json({
freed: {
heapUsed: ((before.heapUsed - after.heapUsed) / 1024 / 1024).toFixed(2) + ' MB',
external: ((before.external - after.external) / 1024 / 1024).toFixed(2) + ' MB'
}
});
});
}
}
setupErrorHandling() {
// 404 handler
this.app.use((req, res) => {
res.status(404).json({ error: 'Not found' });
});
// Error handler
this.app.use((err, req, res, next) => {
console.error('[Error]', err);
res.status(500).json({ error: 'Internal server error' });
});
// Uncaught exception handler
process.on('uncaughtException', (err) => {
console.error('[Uncaught Exception]', err);
this.shutdown(1);
});
// Unhandled rejection handler
process.on('unhandledRejection', (reason, promise) => {
console.error('[Unhandled Rejection]', reason);
});
}
setupGracefulShutdown() {
const signals = ['SIGTERM', 'SIGINT'];
signals.forEach(signal => {
process.on(signal, () => {
console.log(`[Server] Received ${signal}, starting graceful shutdown`);
this.shutdown(0);
});
});
}
async shutdown(exitCode) {
console.log('[Server] Closing HTTP server');
if (this.server) {
this.server.close(() => {
console.log('[Server] HTTP server closed');
// Close monitoring services
if (this.monitoring) {
this.monitoring.close();
}
// Print final GC statistics
console.log('\n=== Final Statistics ===');
this.gcMonitor.printStatistics();
process.exit(exitCode);
});
// Force close after 30 seconds
setTimeout(() => {
console.error('[Server] Forcing shutdown after timeout');
process.exit(1);
}, 30000);
} else {
process.exit(exitCode);
}
}
start() {
this.server = this.app.listen(this.port, () => {
console.log(`[Server] Listening on port ${this.port}`);
console.log(`[Server] PID: ${process.pid}`);
console.log(`[Server] Node version: ${process.version}`);
const memUsage = process.memoryUsage();
console.log(`[Server] Initial memory: ${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`);
});
}
}
// Start server
if (require.main === module) {
const server = new ProductionServer();
server.start();
}
module.exports = ProductionServer;
Kubernetes ConfigMap for Node Options
apiVersion: v1
kind: ConfigMap
metadata:
name: nodejs-config
namespace: production
data:
# Standard configuration
NODE_OPTIONS_STANDARD: |
--max-old-space-size=3072
--max-semi-space-size=64
--expose-gc
# High throughput configuration
NODE_OPTIONS_THROUGHPUT: |
--max-old-space-size=4096
--max-semi-space-size=128
--initial-old-space-size=2048
--optimize-for-size=false
# Low latency configuration
NODE_OPTIONS_LATENCY: |
--max-old-space-size=2048
--max-semi-space-size=32
--expose-gc
--gc-interval=100
# Debug configuration
NODE_OPTIONS_DEBUG: |
--max-old-space-size=2048
--expose-gc
--trace-gc
--trace-gc-verbose
--trace-gc-nvp
Conclusion
Optimizing Node.js memory usage in containerized environments requires understanding V8’s memory model, implementing proper monitoring, and following best practices for memory management. Key takeaways:
- Configure Heap Size Appropriately: Set
--max-old-space-sizeto 75-80% of container memory limit - Monitor Continuously: Implement comprehensive monitoring with Prometheus metrics
- Detect Leaks Early: Use heap snapshots and automated profiling to catch memory leaks
- Use Streams: Process large datasets with streams to avoid loading everything into memory
- Handle GC Properly: Understand and tune garbage collection for your workload
Proper memory optimization can reduce container costs by 30-40% while improving application performance and stability. Regular monitoring and proactive leak detection ensure long-term production reliability.