Home/Building Your First Worker

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:

messages.ts
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:

worker.ts
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:

host.ts
// 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:

host.ts
// 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:

host.ts
// Check worker status
    const status = await worker.send('getStatus', {});
    console.log('Worker status:', status);
host.ts
// 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:

typescript
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:

typescript
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:

typescript
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.