Introduction
Memory leaks and excessive consumption crash applications and increase costs. Understanding Node.js memory behavior and NestJS patterns helps build efficient, stable applications.
Key Concepts
- Heap: Where objects are allocated
- Garbage Collection: Automatic memory reclamation
- Memory Leak: Unreleased references preventing GC
- Request Scoping: Per-request provider instances
Real World Context
A production API that caches user sessions in an unbounded Map will slowly consume all available memory. After days of uptime, the container gets OOM-killed and restarts. Proper memory management prevents these silent production failures.
Deep Dive
Monitor Memory Usage
typescript@Injectable() export class MemoryService { getMemoryUsage() { const used = process.memoryUsage(); return { heapUsed: Math.round(used.heapUsed / 1024 / 1024) + ' MB', heapTotal: Math.round(used.heapTotal / 1024 / 1024) + ' MB', external: Math.round(used.external / 1024 / 1024) + ' MB', rss: Math.round(used.rss / 1024 / 1024) + ' MB', }; } }
Common Leak Sources
typescript// BAD: Growing array @Injectable() export class LeakyService { private cache: any[] = []; // Never cleared! add(item: any) { this.cache.push(item); } } // GOOD: Bounded cache @Injectable() export class BoundedCacheService { private cache = new Map<string, any>(); private maxSize = 1000; set(key: string, value: any) { if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } this.cache.set(key, value); } }
Request-Scoped Memory
typescript// Request scope creates new instance per request @Injectable({ scope: Scope.REQUEST }) export class RequestDataService { private data: any[] = []; // Garbage collected after request }
Stream Large Data
typescript// BAD: Load all into memory async exportAllUsers(): Promise<User[]> { return this.userRepository.find(); // 1M users = OOM } // GOOD: Stream async exportAllUsers(res: Response) { const stream = this.userRepository .createQueryBuilder('user') .stream(); res.setHeader('Content-Type', 'application/json'); stream.pipe(res); }
Cleanup Resources
typescript@Injectable() export class ExternalService implements OnModuleDestroy { private connections: Connection[] = []; onModuleDestroy() { // Close connections on shutdown this.connections.forEach(conn => conn.close()); } }
Common Pitfalls
- Unbounded caches: Always set max size and eviction policy.
- Event listener leaks: Remove listeners when no longer needed.
- Large request bodies: Stream instead of buffering.
Best Practices
- Monitor memory in production
- Use bounded caches with LRU eviction
- Stream large data sets
- Clean up resources in OnModuleDestroy
- Profile with --inspect and Chrome DevTools
Summary
Memory optimization involves monitoring usage, using bounded caches, streaming large data, and cleaning up resources. Request-scoped providers help isolate per-request data. Profile with Node.js inspector to find leaks.
Code Examples
typescript
@Injectable()
export class BoundedCacheService {
private cache = new Map<string, any>();
private maxSize = 1000;
set(key: string, value: any) {
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
}