Iterable<Buffer>;
}
class FileStorageAdapter implements StorageAdapter {
private basePath: string;
constructor(basePath: string) {
this.basePath = basePath;
}
async retrieveRecord(identifier: string): Promise<Buffer> {
const targetPath = ${this.basePath}/${identifier}.dat;
return readFile(targetPath);
}
async persistRecord(identifier: string, payload: Buffer): Promise<void> {
const targetPath = ${this.basePath}/${identifier}.dat;
await writeFile(targetPath, payload);
}
async *streamLargeDataset(query: string): AsyncIterable<Buffer> {
const sourcePath = ${this.basePath}/exports/${query}.csv;
const stream = createReadStream(sourcePath, { highWaterMark: 64 * 1024 });
for await (const chunk of stream) {
yield chunk;
}
}
}
**Architecture Rationale**: Using `fs/promises` eliminates callback nesting and integrates with `async/await` control flow. The `AsyncIterable` pattern for large datasets prevents memory exhaustion by processing data in chunks rather than loading entire files into heap space. This aligns with `libuv`'s thread pool architecture, which handles file operations asynchronously without blocking the main thread.
### Step 2: Implement Request Handler Isolation
Request handlers must never perform synchronous work. Even configuration loading or environment validation should occur during application bootstrap, not during request processing.
```typescript
import { Request, Response, NextFunction } from 'express';
interface RequestContext {
correlationId: string;
startTime: number;
storage: FileStorageAdapter;
}
async function handleDataRetrieval(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
const context: RequestContext = {
correlationId: req.headers['x-correlation-id'] as string || crypto.randomUUID(),
startTime: performance.now(),
storage: req.app.locals.storage as FileStorageAdapter
};
try {
const recordId = req.params.id;
const payload = await context.storage.retrieveRecord(recordId);
res.set({
'X-Request-Id': context.correlationId,
'X-Processing-Ms': String(Math.round(performance.now() - context.startTime))
});
res.status(200).send(payload);
} catch (error) {
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
res.status(404).json({ error: 'Resource not found' });
} else {
next(error);
}
}
}
Architecture Rationale: Error handling is centralized and type-safe. Performance metrics are injected into response headers without synchronous overhead. The handler delegates I/O to the adapter, keeping the main thread available for subsequent requests. This pattern prevents request queue saturation and maintains predictable latency.
Step 3: Offload CPU-Intensive Work
When business logic requires heavy computation (data aggregation, cryptographic operations, image processing), the event loop must be protected. Node.js provides worker_threads for true parallel execution.
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
interface ComputeTask {
algorithm: 'aggregate' | 'transform' | 'validate';
payload: unknown;
}
function executeParallelTask(task: ComputeTask): Promise<unknown> {
return new Promise((resolve, reject) => {
const worker = new Worker(__filename, { workerData: task });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`));
});
});
}
if (!isMainThread) {
const task = workerData as ComputeTask;
const result = processTask(task);
parentPort!.postMessage(result);
}
function processTask(task: ComputeTask): unknown {
switch (task.algorithm) {
case 'aggregate':
return heavyAggregation(task.payload);
case 'transform':
return dataTransformation(task.payload);
default:
throw new Error('Unsupported algorithm');
}
}
Architecture Rationale: CPU-bound operations are isolated from the main thread, preventing event loop starvation. Workers communicate via message passing, maintaining memory safety. This pattern scales computation independently of I/O throughput, allowing the runtime to handle both workloads efficiently.
Pitfall Guide
1. Treating async/await as Parallel Execution
Explanation: Developers often assume that async functions run concurrently. In reality, await pauses execution within that specific function until the promise resolves. Multiple await calls in sequence still execute serially.
Fix: Use Promise.all() or Promise.allSettled() for independent operations. Reserve sequential awaits for dependent operations where output from step A feeds into step B.
2. Synchronous Configuration Reads in Request Handlers
Explanation: Loading environment variables, JSON configs, or feature flags synchronously inside a route handler blocks the event loop for every request.
Fix: Load all static configuration during application bootstrap. Cache values in memory or use a dedicated configuration service. Validate schemas once at startup, not per-request.
3. Unbounded JSON Parsing on Large Payloads
Explanation: JSON.parse() and JSON.stringify() are synchronous and CPU-intensive. Parsing multi-megabyte payloads blocks the main thread, causing latency spikes.
Fix: Implement payload size limits at the gateway level. Use streaming JSON parsers (stream-json, JSONStream) for large datasets. Consider binary formats (MessagePack, Protobuf) for high-throughput internal communication.
4. Ignoring Backpressure in Stream Pipelines
Explanation: Piping data without monitoring backpressure causes memory leaks and buffer overflow. Fast producers overwhelm slow consumers, eventually crashing the process.
Fix: Use stream.pipeline() or stream.compose() which automatically handle backpressure. Monitor stream.writableNeedDrain when manually piping. Implement rate limiting for external data ingestion.
5. Misconfiguring the libuv Thread Pool
Explanation: The default thread pool size (4) is insufficient for high-concurrency I/O workloads. File operations, DNS lookups, and crypto functions compete for limited threads, causing queue buildup.
Fix: Adjust UV_THREADPOOL_SIZE based on expected concurrent I/O operations. Monitor thread pool utilization with process.binding('uv').getMetrics(). Scale proportionally to CPU cores and expected I/O concurrency.
6. Mixing Callbacks, Promises, and Async Iterators Inconsistently
Explanation: Hybrid control flow creates unpredictable error boundaries and makes stack traces unreadable. Callback-based libraries often lack proper rejection handling.
Fix: Standardize on async/await across the codebase. Wrap legacy callback APIs using util.promisify(). Use try/catch blocks consistently and avoid mixing .then() chains with await.
7. Blocking the Event Loop with Synchronous Crypto Operations
Explanation: Functions like crypto.randomBytesSync() or crypto.pbkdf2Sync() block the main thread. Hashing passwords or generating tokens synchronously under load degrades responsiveness.
Fix: Use asynchronous crypto methods (crypto.randomBytes(), crypto.pbkdf2()). Offload password hashing to worker threads or dedicated authentication services. Cache frequently used cryptographic results when security permits.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| I/O-bound API (DB, external services) | Async non-blocking with connection pooling | Maximizes event loop availability; scales horizontally | Low infrastructure cost; high throughput ROI |
| CPU-bound processing (aggregation, encryption) | worker_threads or dedicated microservice | Prevents event loop starvation; isolates compute resources | Higher memory overhead; requires process management |
| Large file/data transfer | Streaming with backpressure handling | Prevents heap exhaustion; maintains stable latency | Moderate implementation complexity; reduces memory costs |
| Startup configuration loading | Synchronous reads during bootstrap | One-time cost; simplifies runtime logic | Negligible; improves runtime performance |
| Real-time WebSocket messaging | Async event-driven with message queuing | Maintains low latency; handles connection spikes gracefully | Requires message broker; scales efficiently |
Configuration Template
// server.ts
import express from 'express';
import { performance } from 'perf_hooks';
import { FileStorageAdapter } from './adapters/file-storage';
import { handleDataRetrieval } from './handlers/data-retrieval';
const app = express();
const PORT = process.env.PORT || 3000;
// Bootstrap configuration synchronously (acceptable at startup)
const config = {
storagePath: process.env.STORAGE_PATH || './data',
maxPayloadSize: '10mb',
requestTimeout: 30000
};
// Initialize adapters
const storageAdapter = new FileStorageAdapter(config.storagePath);
app.locals.storage = storageAdapter;
// Middleware
app.use(express.json({ limit: config.maxPayloadSize }));
app.use((req, res, next) => {
res.setHeader('X-Server-Start', new Date().toISOString());
next();
});
// Event loop monitoring
setInterval(() => {
const start = performance.now();
setImmediate(() => {
const lag = performance.now() - start;
if (lag > 10) {
console.warn(`Event loop lag detected: ${lag.toFixed(2)}ms`);
}
});
}, 5000);
// Routes
app.get('/api/records/:id', handleDataRetrieval);
// Error boundary
app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
console.error('Unhandled error:', err.message);
res.status(500).json({ error: 'Internal server error' });
});
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
Quick Start Guide
- Initialize Project: Run
npm init -y && npm install express typescript @types/node @types/express ts-node
- Configure TypeScript: Create
tsconfig.json with "target": "ES2022", "module": "commonjs", "strict": true, and "outDir": "./dist"
- Create Async Adapter: Implement a storage or network adapter using
fs/promises or fetch with proper error handling and timeout configuration
- Build Request Handler: Write an Express/Fastify route that delegates I/O to the adapter, uses
async/await, and returns structured JSON responses
- Validate Non-Blocking Behavior: Run
autocannon -c 100 -d 10 http://localhost:3000/api/records/test and verify latency remains stable under concurrent load
This execution model transforms Node.js from a simple scripting environment into a high-throughput I/O multiplexer. By respecting the event loop's single-threaded nature and delegating waiting periods to background workers, systems achieve predictable latency, efficient resource utilization, and horizontal scalability without sacrificing developer ergonomics.