Worker Lifecycle
Understanding of worker lifecycle helps you build robust applications that properly manage resources and handle failures gracefully.
Lifecycle Phases
A worker goes through these phases:
createWorker() → Starting → Connected → Active → Closing → Closed
↓ ↓ ↓
Error Error Error- Starting: Process spawned, waiting for socket connection
- Connected: Socket established, ready to receive messages
- Active: Processing messages normally
- Closing: Graceful shutdown initiated
- Closed: Process terminated, resources released
Checking Worker Status
The worker client exposes two status properties:
function printStatus(worker: WorkerClient) {
console.log(` PID: ${worker.pid}`);
console.log(` isActive: ${worker.isActive}`);
console.log(` isConnected: ${worker.isConnected}`);
}| Property | Description |
|---|---|
isActive | true if worker process is alive AND connection is active |
isConnected | true if both local and channel connection states are active |
A healthy worker has both isActive: true and isConnected: true.
Worker State Persistence
Workers maintain state across messages. Variables defined at the module level persist:
// Worker state - persists across requests
const startTime = Date.now();
let requestCount = 0;
let counter = 0;Each message handler can read and modify this shared state:
const handlers: Handlers<Messages> = {
getStatus: () => {
requestCount++;
return {
uptime: Date.now() - startTime,
requestCount,
};
},
incrementCounter: ({ amount }) => {
requestCount++;
counter += amount;
console.log(`Worker: Counter incremented by ${amount}, now ${counter}`);
return { newValue: counter };
},
};This is useful for:
- Caching computed results
- Tracking request counts
- Maintaining database connections
- Storing configuration loaded at startup
Graceful Shutdown
Always close workers when done to release resources:
const worker = await createWorker<Messages>({ script: './worker.js' });
try {
// Use the worker
await worker.send('doWork', { data });
} finally {
// Always close, even if an error occurred
await worker.close();
}The close() method:
- Rejects all pending requests (they won't complete)
- Stops accepting new messages
- Sends SIGTERM to worker process
- Waits up to 5 seconds for graceful exit
- Force-kills worker if it doesn't exit
- Cleans up socket files
Sending After Close
Attempting to send a message after closing throws an error:
// Attempting to send after close should fail
console.log('--- Attempting Request After Close ---');
try {
await worker.send('getStatus', {});
console.log('ERROR: Should have thrown!');
} catch (err) {
console.log(`Expected error: ${(err as Error).message}`);
}Check isActive before sending if you're unsure of the worker's state:
if (worker.isActive) {
await worker.send('message', payload);
} else {
console.log('Worker is not available');
}Timeout Configuration
Workers support built-in timeout keys for controlling worker startup, connection, and message timeouts:
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)
For detailed timeout setup and per-message-type overrides, see Timeout Configuration.
Handling Worker Crashes
If the worker process crashes unexpectedly:
isActivebecomesfalseisConnectedbecomesfalse- Pending messages reject with an error
- The socket is cleaned up automatically
Detect crashes by checking status or catching send errors:
try {
await worker.send('work', data);
} catch (error) {
if (!worker.isActive) {
console.log('Worker crashed, restarting...');
worker = await createWorker<Messages>({ script: './worker.js' });
await worker.send('work', data);
}
}Worker-Side Shutdown Handling
Workers can handle SIGTERM to clean up before exit:
process.on('SIGTERM', async () => {
console.log('Shutting down gracefully...');
// Close database connections
await db.close();
// Stop the worker server
await server.stop();
process.exit(0);
});Best Practices
1. Use try/finally for Cleanup
const worker = await createWorker<Messages>({ script: './worker.js' });
try {
await processAllItems(worker);
} finally {
await worker.close();
}2. Check Status Before Long Operations
async function processItems(worker: WorkerClient<Messages>, items: Item[]) {
for (const item of items) {
if (!worker.isActive) {
throw new Error('Worker died during processing');
}
await worker.send('process', item);
}
}3. Implement Health Checks
async function healthCheck(worker: WorkerClient<Messages>): Promise<boolean> {
if (!worker.isActive || !worker.isConnected) {
return false;
}
try {
await worker.send('ping', {});
return true;
} catch {
return false;
}
}4. Don't Store Worker References Long-Term
Workers can crash. Store the creation parameters instead:
// Instead of storing the worker
const workerConfig = { script: './worker.js', timeout: 10000 };
async function getWorker() {
return createWorker<Messages>(workerConfig);
}See Also
- {% example-link worker-lifecycle %} - Complete lifecycle example
- Timeout Configuration - Configuring startup and message timeouts
- Error Handling - Handling worker errors