Storage manager! Consul Caesar.js noticed that our fort is becoming overloaded - warehouses are bursting at the seams and the legion has less and less room to maneuver. It's time to learn memory management in Node.js!
Imagine the application as a warehouse system:
Good memory management = an efficient, fast fort!
1// memory-monitor.service.ts
2import { Injectable } from '@nestjs/common';
3import { Cron } from '@nestjs/schedule';
4
5@Injectable()
6export class MemoryMonitorService {
7 private memoryHistory: Array<{
8 timestamp: Date;
9 usage: NodeJS.MemoryUsage;
10 external: number;
11 }> = [];
12
13 private readonly MEMORY_THRESHOLD_MB = 512; // Alert at 512MB
14 private readonly HISTORY_SIZE = 100;
15
16 @Cron('*/10 * * * * *') // Every 10 seconds
17 checkMemoryUsage(): void {
18 const usage = process.memoryUsage();
19 const externalMemory = this.getExternalMemory();
20
21 this.memoryHistory.push({
22 timestamp: new Date(),
23 usage,
24 external: externalMemory,
25 });
26
27 // Keep only the last 100 measurements
28 if (this.memoryHistory.length > this.HISTORY_SIZE) {
29 this.memoryHistory.shift();
30 }
31
32 // Check if memory exceeds the threshold
33 const heapUsedMB = usage.heapUsed / 1024 / 1024;
34 if (heapUsedMB > this.MEMORY_THRESHOLD_MB) {
35 console.warn(` High memory usage: ${heapUsedMB.toFixed(2)}MB`);
36 this.suggestOptimizations(usage);
37 }
38
39 // Log every minute
40 if (new Date().getSeconds() === 0) {
41 this.logMemoryStatus(usage);
42 }
43 }
44
45 private getExternalMemory(): number {
46 // Approximate external memory (buffers, C++ addons)
47 return process.memoryUsage().external || 0;
48 }
49
50 private logMemoryStatus(usage: NodeJS.MemoryUsage): void {
51 const formatMB = (bytes: number) => (bytes / 1024 / 1024).toFixed(2);
52
53 console.log(' Fort storage status:');
54 console.log(` Heap used: ${formatMB(usage.heapUsed)}MB`);
55 console.log(` Heap total: ${formatMB(usage.heapTotal)}MB`);
56 console.log(` RSS (physical memory): ${formatMB(usage.rss)}MB`);
57 console.log(` External: ${formatMB(usage.external)}MB`);
58
59 // Calculate memory growth
60 const growthInfo = this.calculateMemoryGrowth();
61 if (growthInfo.isGrowing) {
62 console.log(`Trend: +${growthInfo.growthRate.toFixed(2)}MB/min`);
63 }
64 }
65
66 private calculateMemoryGrowth(): { isGrowing: boolean; growthRate: number } {
67 if (this.memoryHistory.length < 10) return { isGrowing: false, growthRate: 0 };
68
69 const recent = this.memoryHistory.slice(-10);
70 const oldest = recent[0];
71 const newest = recent[recent.length - 1];
72
73 const timeDiffMin = (newest.timestamp.getTime() - oldest.timestamp.getTime()) / 60000;
74 const memoryDiffMB = (newest.usage.heapUsed - oldest.usage.heapUsed) / 1024 / 1024;
75
76 const growthRate = memoryDiffMB / timeDiffMin;
77
78 return {
79 isGrowing: growthRate > 1, // More than 1MB/min
80 growthRate,
81 };
82 }
83
84 private suggestOptimizations(usage: NodeJS.MemoryUsage): void {
85 console.log(' Optimization suggestions:');
86
87 if (usage.heapUsed / usage.heapTotal > 0.9) {
88 console.log(' - Run garbage collection: global.gc()');
89 }
90
91 if (usage.external > 50 * 1024 * 1024) {
92 console.log(' - Check buffers and streams - perhaps too much data in memory');
93 }
94
95 console.log(' - Check for potential memory leaks in caches');
96 console.log(' - Consider reducing TTL for temporary data');
97 }
98
99 getMemoryReport() {
100 const current = process.memoryUsage();
101 const formatMB = (bytes: number) => (bytes / 1024 / 1024).toFixed(2);
102
103 return {
104 current: {
105 heapUsed: formatMB(current.heapUsed) + 'MB',
106 heapTotal: formatMB(current.heapTotal) + 'MB',
107 rss: formatMB(current.rss) + 'MB',
108 external: formatMB(current.external) + 'MB',
109 },
110 trend: this.calculateMemoryGrowth(),
111 history: this.memoryHistory.slice(-20), // Last 20 measurements
112 alerts: this.generateAlerts(current),
113 };
114 }
115
116 private generateAlerts(usage: NodeJS.MemoryUsage): string[] {
117 const alerts = [];
118 const heapUsedMB = usage.heapUsed / 1024 / 1024;
119
120 if (heapUsedMB > this.MEMORY_THRESHOLD_MB) {
121 alerts.push(`High heap usage: ${heapUsedMB.toFixed(2)}MB`);
122 }
123
124 if (usage.heapUsed / usage.heapTotal > 0.85) {
125 alerts.push('Heap is 85% full - consider garbage collection');
126 }
127
128 const growthInfo = this.calculateMemoryGrowth();
129 if (growthInfo.isGrowing && growthInfo.growthRate > 5) {
130 alerts.push(`Rapid memory growth: ${growthInfo.growthRate.toFixed(2)}MB/min`);
131 }
132
133 return alerts;
134 }
135
136 // Force garbage collection (wymaga --expose-gc flag)
137 forceGarbageCollection(): { before: number; after: number; freed: number } {
138 const beforeMB = process.memoryUsage().heapUsed / 1024 / 1024;
139
140 if (global.gc) {
141 global.gc();
142 const afterMB = process.memoryUsage().heapUsed / 1024 / 1024;
143 const freedMB = beforeMB - afterMB;
144
145 console.log(`Garbage collection: freed ${freedMB.toFixed(2)}MB`);
146
147 return {
148 before: beforeMB,
149 after: afterMB,
150 freed: freedMB,
151 };
152 } else {
153 console.warn('Garbage collection unavailable - run with --expose-gc flag');
154 return { before: beforeMB, after: beforeMB, freed: 0 };
155 }
156 }
157}1// memory-optimized-cache.service.ts
2@Injectable()
3export class MemoryOptimizedCacheService {
4 private cache = new Map<string, {
5 data: any;
6 timestamp: number;
7 accessCount: number;
8 lastAccess: number;
9 size: number;
10 }>();
11
12 private maxSize = 1000; // Maximum number of elements
13 private maxMemoryMB = 100; // Maximum cache memory
14 private currentMemoryBytes = 0;
15
16 set(key: string, value: any, ttl: number = 300000): void {
17 const size = this.estimateSize(value);
18
19 // Check if adding a new element will exceed the limits
20 if (this.cache.size >= this.maxSize ||
21 (this.currentMemoryBytes + size) > this.maxMemoryMB * 1024 * 1024) {
22 this.evictOldest();
23 }
24
25 // Remove old element if it exists
26 if (this.cache.has(key)) {
27 const old = this.cache.get(key)!;
28 this.currentMemoryBytes -= old.size;
29 }
30
31 this.cache.set(key, {
32 data: value,
33 timestamp: Date.now(),
34 accessCount: 0,
35 lastAccess: Date.now(),
36 size,
37 });
38
39 this.currentMemoryBytes += size;
40
41 // Set TTL
42 setTimeout(() => {
43 this.delete(key);
44 }, ttl);
45 }
46
47 get(key: string): any {
48 const item = this.cache.get(key);
49 if (!item) return null;
50
51 // Update access statistics
52 item.accessCount++;
53 item.lastAccess = Date.now();
54
55 return item.data;
56 }
57
58 delete(key: string): boolean {
59 const item = this.cache.get(key);
60 if (item) {
61 this.currentMemoryBytes -= item.size;
62 return this.cache.delete(key);
63 }
64 return false;
65 }
66
67 // Eviction using different strategies
68 private evictOldest(): void {
69 if (this.cache.size === 0) return;
70
71 const strategy = this.getEvictionStrategy();
72
73 switch (strategy) {
74 case 'LRU': // Least Recently Used
75 this.evictLRU();
76 break;
77 case 'LFU': // Least Frequently Used
78 this.evictLFU();
79 break;
80 case 'SIZE': // Largest elements first
81 this.evictLargest();
82 break;
83 default:
84 this.evictOldestByTime();
85 }
86 }
87
88 private getEvictionStrategy(): string {
89 // Intelligent strategy selection based on memory state
90 const memoryPressure = this.currentMemoryBytes / (this.maxMemoryMB * 1024 * 1024);
91
92 if (memoryPressure > 0.9) {
93 return 'SIZE'; // Under high memory pressure - remove large elements
94 } else if (this.cache.size > this.maxSize * 0.8) {
95 return 'LRU'; // Many elements - remove least recently used
96 } else {
97 return 'LFU'; // Normally - remove least popular
98 }
99 }
100
101 private evictLRU(): void {
102 let oldestKey = '';
103 let oldestTime = Date.now();
104
105 this.cache.forEach((item, key) => {
106 if (item.lastAccess < oldestTime) {
107 oldestTime = item.lastAccess;
108 oldestKey = key;
109 }
110 });
111
112 if (oldestKey) {
113 console.log(`LRU eviction: ${oldestKey}`);
114 this.delete(oldestKey);
115 }
116 }
117
118 private evictLFU(): void {
119 let leastUsedKey = '';
120 let leastUsedCount = Infinity;
121
122 this.cache.forEach((item, key) => {
123 if (item.accessCount < leastUsedCount) {
124 leastUsedCount = item.accessCount;
125 leastUsedKey = key;
126 }
127 });
128
129 if (leastUsedKey) {
130 console.log(`LFU eviction: ${leastUsedKey}`);
131 this.delete(leastUsedKey);
132 }
133 }
134
135 private evictLargest(): void {
136 let largestKey = '';
137 let largestSize = 0;
138
139 this.cache.forEach((item, key) => {
140 if (item.size > largestSize) {
141 largestSize = item.size;
142 largestKey = key;
143 }
144 });
145
146 if (largestKey) {
147 console.log(`SIZE eviction: ${largestKey} (${largestSize} bytes)`);
148 this.delete(largestKey);
149 }
150 }
151
152 private evictOldestByTime(): void {
153 let oldestKey = '';
154 let oldestTime = Date.now();
155
156 this.cache.forEach((item, key) => {
157 if (item.timestamp < oldestTime) {
158 oldestTime = item.timestamp;
159 oldestKey = key;
160 }
161 });
162
163 if (oldestKey) {
164 console.log(`TIME eviction: ${oldestKey}`);
165 this.delete(oldestKey);
166 }
167 }
168
169 private estimateSize(obj: any): number {
170 // Approximate calculation of object size in memory
171 const jsonString = JSON.stringify(obj);
172 return jsonString.length * 2; // UTF-16 = 2 bytes per character
173 }
174
175 getStats() {
176 const memoryUsageMB = this.currentMemoryBytes / 1024 / 1024;
177 const avgItemSize = this.cache.size > 0 ? this.currentMemoryBytes / this.cache.size : 0;
178
179 let totalAccesses = 0;
180 this.cache.forEach(item => {
181 totalAccesses += item.accessCount;
182 });
183
184 return {
185 size: this.cache.size,
186 maxSize: this.maxSize,
187 memoryUsageMB: memoryUsageMB.toFixed(2),
188 maxMemoryMB: this.maxMemoryMB,
189 avgItemSize: Math.round(avgItemSize),
190 totalAccesses,
191 fillRatio: (this.cache.size / this.maxSize * 100).toFixed(1) + '%',
192 memoryRatio: (memoryUsageMB / this.maxMemoryMB * 100).toFixed(1) + '%',
193 };
194 }
195
196 // Cleaning up expired elements
197 @Cron('*/5 * * * * *') // Every 5 minutes
198 cleanupExpired(): void {
199 const now = Date.now();
200 const toDelete = [];
201
202 this.cache.forEach((item, key) => {
203 // Remove elements older than an hour without access
204 if (now - item.lastAccess > 3600000) {
205 toDelete.push(key);
206 }
207 });
208
209 toDelete.forEach(key => {
210 this.delete(key);
211 });
212
213 if (toDelete.length > 0) {
214 console.log(`🧹 Cleaned up ${toDelete.length} expired cache elements`);
215 }
216 }
217}