# SandboxedWorker: Isolated Job Processing

The experimental bunqueue SandboxedWorker runs handlers in worker threads on Bun, Node.js, and Deno so CPU-heavy jobs cannot block the main loop.

Canonical: https://bunqueue.dev/guide/worker/sandboxed/

---

import { Tabs, TabItem } from '@astrojs/starlight/components';

<div class="bq-wrap bq-hero">
  <span class="bq-eyebrow">guide · worker</span>
  <h1 class="bq-hero-h1 bq-bench-h1">Handlers in <em>their own thread.</em></h1>
  <p class="bq-hero-sub">CPU-bound work starves an event loop. SandboxedWorker moves the handler into a worker thread so the main thread can keep heartbeating, using the same experimental worker-pool implementation on Bun, Node.js, and Deno.</p>
</div>

## SandboxedWorker

:::danger[Experimental, not recommended for production]
Treat `SandboxedWorker` as an opt-in experimental feature. Pin
and test the runtime version you deploy, and prefer a standard `Worker` plus a
supervised process pool for production workloads.
:::

`SandboxedWorker` runs a processor module in Bun Workers or portable worker threads on Node.js and Deno. The queue and
heartbeat loop stay in the parent thread. A per-job timeout terminates a stuck
thread, and a crashed thread is restarted only while `autoRestart` is enabled
and its restart budget remains.

This is execution separation, **not a security sandbox**. Threads share the same
OS process and authority; do not use it to run untrusted code, and do not assume
an out-of-memory failure is contained to one thread.

## Availability

<Tabs syncKey="lang">
<TabItem label="Bun">

```typescript
import { SandboxedWorker } from 'bunqueue/client';

const worker = new SandboxedWorker('cpu-intensive', {
  processor: './processor.ts',  // Path to processor file
  concurrency: 4,               // 4 parallel worker threads
  timeout: 60000,               // Per-job timeout (default: 30000, 0 = disabled)
  maxMemory: 256,               // compatibility hint; <= 64 enables smol mode
});

await worker.start();
```

</TabItem>
<TabItem label="Node.js / Deno">

```typescript
import { SandboxedWorker } from 'bunqueue-client';

const worker = new SandboxedWorker('cpu-intensive', {
  processor: './processor.js', // Compiled ESM processor module
  connection: { host: '127.0.0.1', port: 6789 },
  concurrency: 4,
  timeout: 60_000,
});
await worker.start();
```

Node.js and Deno use the portable worker-thread adapter. The processor must
be executable by the host runtime; compile TypeScript to JavaScript when needed.
Cloudflare Workers cannot create this local thread pool.

</TabItem>
<TabItem label="Python">

The Python SDK does not export `SandboxedWorker`. Delegate the calculation to a
`ProcessPoolExecutor`; the normal Worker retains the lease and heartbeat loop:

```python
pool = ProcessPoolExecutor(max_workers=4)
worker = Worker("cpu-intensive", lambda job: pool.submit(run_cpu, job.data).result(),
                concurrency=4, heartbeat_interval_s=10.0, lock_ttl_ms=60_000)
```

</TabItem>
<TabItem label="PHP">

The PHP SDK does not export `SandboxedWorker`. Its Worker is sequential, so use
a supervised child process/service and renew the lease during long waits:

```php
$worker = new Worker('cpu-intensive', function (Bunqueue\Job $job) {
    $job->extendLock(60000);
    return runInChildProcess($job->data());
}, ['lockTtlMs' => 60000]);
```

</TabItem>
<TabItem label="Go">

The Go SDK does not export `SandboxedWorker`. Processors already run in a
bounded goroutine pool and the heartbeat loop is separate:

```go
worker := bunqueue.NewWorker("cpu-intensive", processor, bunqueue.WorkerOptions{
    Concurrency: 4, LockTtlMs: 60_000, HeartbeatIntervalS: 10,
})
```

</TabItem>
<TabItem label="Rust">

The Rust SDK does not export `SandboxedWorker`. Its standard Worker runs
processors on worker threads and heartbeats independently:

```rust
let worker = Worker::new("cpu-intensive", processor, WorkerOptions {
    concurrency: 4,
    lock_ttl_ms: 60_000,
    heartbeat_interval: Some(Duration::from_secs(10)),
    ..Default::default()
});
```

</TabItem>
<TabItem label="Elixir">

The Elixir SDK does not export `SandboxedWorker`. The standard Worker runs each
handler in a Task and heartbeats from another process; isolate blocking NIFs in
a dirty scheduler or external port:

```elixir
worker = Bunqueue.Worker.new("cpu-intensive", handler,
  concurrency: 4, lock_ttl: 60_000, heartbeat_interval: 10_000)
```

</TabItem>
</Tabs>

The remaining API is shared by both TypeScript packages. A local thread pool
requires Bun, Node.js, or Deno; it is unavailable inside Cloudflare Workers.

## Processor module

**Processor file** (`processor.ts`):

```typescript
export default async (job: {
  id: string;
  data: any;
  queue: string;
  attempts: number;
  parentId?: string;
  progress: (value: number) => void;
  log: (message: string) => void;
  fail: (error: string | Error) => void;
}) => {
  job.progress(50);
  const result = await heavyComputation(job.data);
  job.progress(100);
  return result;
};
```

To connect to a remote server instead of running embedded, pass a `connection`
option (`host`, `port`, `token`); otherwise the shared embedded manager is used.

## Lifecycle and local stats

```typescript
await worker.start();
worker.isRunning();
const stats = worker.getStats(); // { total, busy, idle, recycled, restarts }
await worker.stop();             // Graceful (waits for busy workers)
await worker.stop(true);         // Force
```

`getStats()` reports pool bookkeeping in the current process. It is not a broker
metrics snapshot.

### SandboxedWorker options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `processor` | `string` | (required) | Path to processor file |
| `concurrency` | `number` | `1` | Parallel worker threads |
| `maxMemory` | `number` | `256` | Compatibility hint: values `<= 64` enable Bun's `smol` Worker mode. This implementation does **not** enforce an MB memory limit |
| `timeout` | `number` | `30000` | Per-job timeout in ms (0 = disabled) |
| `autoRestart` | `boolean` | `true` | Auto-restart crashed threads |
| `maxRestarts` | `number` | `10` | Restart budget per thread; the counter increments before the check, so `10` allows 9 actual restarts |
| `pollInterval` | `number` | `10` | Sleep in ms when no idle thread is available; job pulls themselves use a fixed 1000 ms broker long-poll |
| `heartbeatInterval` | `number` | `5000` (embedded) / `10000` (TCP) | Heartbeat for stall detection and lock renewal; non-positive disables it |
| `idleTimeout` | `number` | `0` | Stop the pool after this many idle ms (0 = disabled) |
| `idleRecycleMs` | `number` | `30000` | Recycle idle threads after this many ms (0 = disabled) |
| `autoStart` | `boolean` | `false` | Restart the pool when new jobs arrive after an idle shutdown |
| `autoStartPollMs` | `number` | `5000` | Poll interval while idle-stopped |
| `connection` | `ConnectionOptions` | - | TCP connection (omit for embedded) |

SandboxedWorker emits eight local events: `ready`, `active`, `completed`,
`failed`, `progress`, `log`, `error`, and `closed`. It does **not** emit
`stalled`, `drained`, or `cancelled`. `completed`/`failed` fire only after the
broker confirms the ACK/FAIL as applied; if the broker reports the job as
already finalized no event fires, and an ACK/FAIL transport error emits `error`
instead.

### Worker vs SandboxedWorker

| Comparison | Worker | SandboxedWorker |
|---|--------|-----------------|
| **Production ready** | ✅ Stable | ⚠️ Experimental bunqueue implementation |
| **I/O-bound tasks** (HTTP, DB, APIs) | ✅ Best choice | Overkill |
| **CPU-intensive tasks** | ⚠️ Blocks event loop | ✅ Runs in separate thread |
| **Untrusted code** | ❌ Not isolated | ❌ Thread separation is not a security boundary |
| **Per-thread memory limit** | ❌ | ❌ `maxMemory` does not enforce one |
| **Events** | 11 events | 8 events |
| **Concurrency, retries, heartbeats** | ✅ | ✅ Supported through a separate implementation |

Most workloads are I/O-bound (API calls, database queries, file operations); for
those, `Worker` is the right choice. For CPU-heavy work, see
[CPU-Intensive Workers](/guide/cpu-intensive-workers/) for the supported
offloading and lease-sizing patterns.

:::tip[Related Guides]
- [Queue API](/guide/queue/), add and manage jobs
- [Stall Detection & Recovery](/guide/stall-detection/), handle unresponsive workers
- [Monitoring & Prometheus Metrics](/guide/monitoring/), watch worker performance
:::

## Where to go next

| Guide | What it covers |
|---|---|
| [Worker](/guide/worker/) | Create a worker and process your first job |
| [Worker Concurrency and Batch Pulling](/guide/worker/concurrency/) | Run jobs in parallel and pull them in batches |
| [The Job Object Inside a Worker Processor](/guide/worker/job-object/) | Everything the processor receives and can do |
| [Worker Events](/guide/worker/events/) | completed, failed, stalled and the rest |
| [Worker Error Handling, Retries and Backoff](/guide/worker/errors/) | Retries, backoff, timeouts and giving up |
| [Worker Lifecycle](/guide/worker/lifecycle/) | Pause, resume and shut down without losing work |
| [Heartbeats, Stall Detection and Lock Ownership](/guide/worker/stalls/) | Heartbeats, stall recovery and lock ownership |
| [WorkerOptions Reference](/guide/worker/options/) | Every WorkerOptions field, with defaults |