Building Your First Worker
A complete guide to building a production-ready worker for CPU-intensive tasks.
Use Case: Image Processing Worker
We'll build a worker that handles image processing operations. This demonstrates the key benefits of isolated-workers: offloading CPU-intensive work while maintaining type safety.
Step 1: Define the Message Contract
Create a shared types file that both host and worker will use:
export type Messages = DefineMessages<{
// Process an image and return metadata
processImage: {
payload: {
imagePath: string;
options: { grayscale: boolean; quality: number };
};
result: {
width: number;
height: number;
format: string;
size: number;
};
};
// Batch process multiple images
batchProcess: {
payload: {
paths: string[];
options: { grayscale: boolean; quality: number };
};
result: {
successful: number;
failed: number;
results: Array<{ path: string; success: boolean }>;
};
};
// Get current worker status
getStatus: {
payload: Record<string, never>;
result: {
active: boolean;
processedCount: number;
};
};
}>;Step 2: Implement the Worker
Create the worker script with handlers for each message type:
const handlers: Handlers<Messages> = {
processImage: async ({ imagePath }) => {
// Simulate image processing (replace with real logic)
console.log(`Processing: ${imagePath}`);
// In a real implementation, you would use sharp, jimp, or similar
await simulateProcessing();
processedCount++;
return {
width: 1920,
height: 1080,
format: 'jpeg',
size: 256000,
};
},
batchProcess: async ({ paths }) => {
const results = [];
for (const path of paths) {
try {
await simulateProcessing();
results.push({ path, success: true });
} catch {
results.push({ path, success: false });
}
}
return {
successful: results.filter((r) => r.success).length,
failed: results.filter((r) => !r.success).length,
results,
};
},
getStatus: async () => {
return {
active: true,
processedCount,
};
},
};Step 3: Create the Host Process
Create the main process that spawns and communicates with the worker. Note how we configure per-operation timeouts:
// Spawn the worker with per-operation timeouts
const worker = await createWorker<Messages>({
script: join(__dirname, 'worker.ts'),
// Configure timeouts for different operations
timeout: {
WORKER_STARTUP: 5000, // 5s to start
processImage: 30000, // 30s per image
batchProcess: 300000, // 5min for batch
},
});Now use the worker to process images:
// Process a single image
const metadata = await worker.send('processImage', {
imagePath: './photo.jpg',
options: { grayscale: true, quality: 85 },
});
console.log('Image metadata:', metadata);Check worker status and batch process multiple images:
// Check worker status
const status = await worker.send('getStatus', {});
console.log('Worker status:', status);// Batch process multiple images
const batchResult = await worker.send('batchProcess', {
paths: ['./a.jpg', './b.jpg', './c.jpg'],
options: { grayscale: false, quality: 90 },
});
console.log('Batch result:', batchResult);Built-in Timeout Keys
isolated-workers provides three built-in timeout keys:
WORKER_STARTUP: Time to wait for worker process to start (default: 10 seconds)SERVER_CONNECT: Time for server to wait for host connection (default: 30 seconds)WORKER_MESSAGE: Default timeout for all messages (default: 5 minutes)
Per-message-type timeouts (like processImage) override WORKER_MESSAGE for specific operations.
Example:
const timeout: TimeoutConfig<Messages> = {
// Built-in keys
WORKER_STARTUP: 5000, // 5s to start
SERVER_CONNECT: 10000, // 10s to connect
WORKER_MESSAGE: 60000, // 1min default for messages
// Per-message overrides
processImage: 30000, // 30s for image processing
batchProcess: 300000, // 5min for batch operations
};Error Handling
Always handle errors when working with workers:
const worker = await createWorker<Messages>({
script: './worker.js',
});
try {
const result = await worker.send('add', { a: 5, b: 3 });
console.log(result.sum);
} catch (err) {
// Handle worker errors (timeout, process crash, handler error)
console.error('Worker error:', err.message);
} finally {
// Always close the worker when done
await worker.close();
}Graceful Shutdown
Always close workers when done to ensure clean shutdown:
const worker = await createWorker<Messages>({
script: './worker.js',
});
try {
await doWork(worker);
} finally {
// Always close to terminate worker process and clean up resources
await worker.close();
}The close() method:
- Rejects all pending requests
- Sends SIGTERM to worker process
- Waits up to 5 seconds for graceful exit
- Force-kills if needed
- Cleans up socket files
This prevents resource leaks and orphaned processes.
Key Features Demonstrated
- Type Safety: Payload and result types are fully checked
- Timeouts: Per-message-type timeout configuration
- Error Handling: Try/finally ensures proper cleanup
- Worker State: Worker maintains internal state between messages
Next Steps
Explore the Guides to learn more about advanced patterns, or check out the Examples for more complete implementations.