Skip to content
Get started
Get started

CPU-Intensive Workers over TCP in Bunqueue

guide · cpu-intensive-workers

CPU-intensive workers, off the event loop.

Synchronous CPU-heavy work over TCP blocks the event loop, drops the connection, and requeues every in-flight job. This page explains why it happens and how to configure workers to avoid it.

The TCP client sends periodic ping health checks (default: every 30s). Under heavy CPU load, the event loop can’t process these pings in time. After 3 consecutive failures (~90s), the client triggers a forced reconnect, which closes the socket. When the server detects the socket close, it calls releaseClientJobs() and requeues all processing jobs. The worker then fails to ACK completed jobs with:

Error: Job not found or not in processing state

Disable the ping health check and increase the command timeout:

const worker = new Worker('heavy-queue', processor, {
concurrency: 3,
connection: {
port: 6789,
pingInterval: 0, // Disable ping health check
commandTimeout: 60000, // Increase command timeout to 60s
},
useLocks: false, // Avoid lock expiration under load
heartbeatInterval: 0, // Disable heartbeat
});

Even with pings disabled, long synchronous CPU work blocks heartbeats, lock renewals, and TCP responses. Break up CPU-heavy loops with periodic yields:

// Bad: blocks event loop for entire duration
function findNthPrime(n: number): number {
let count = 0, candidate = 1;
while (count < n) {
candidate++;
if (isPrime(candidate)) count++;
}
return candidate;
}
// Good: yields every 500 iterations
async function findNthPrime(n: number): Promise<number> {
let count = 0, candidate = 1, ops = 0;
while (count < n) {
candidate++;
if (isPrime(candidate)) count++;
if (++ops % 500 === 0) await Bun.sleep(0);
}
return candidate;
}

await Bun.sleep(0) yields to the event loop for one tick, allowing timers, TCP I/O, and heartbeats to fire.

SettingDefaultEffect under CPU load
pingInterval30000ms3 consecutive failures → forced reconnect (~90s)
commandTimeout30000msLong-running commands timeout (3 consecutive timeouts also force a reconnect)
lockDuration30000msJob lock expires if heartbeats cannot renew it in time
stallInterval30000msJob marked stalled if no heartbeat

For truly CPU-bound work where yielding is not practical, SandboxedWorker runs each job in an isolated Bun Worker thread, so the main event loop is never blocked:

import { SandboxedWorker } from 'bunqueue/client';
const worker = new SandboxedWorker('heavy-queue', {
processor: './heavy-processor.ts',
concurrency: 4,
connection: { port: 6789 },
});
await worker.start();