# Tune the worker runtime

Trade throughput against isolation by tuning four real knobs — worker **concurrency**, the **runner mode**, per-task **Deno permissions**, and **timeouts/retries** — without touching a single job handler.

The same `process-payment` handler runs unchanged whether you give it one in-process slot or ten subprocess-isolated workers. Everything below is a deployment setting: it lives in `config/official-plugins/mod.ts` (`defineWorkers(...)`), in per-task runtime config, or in an environment variable read by the worker entrypoint. See [Background jobs](https://rickylabs.github.io/netscript/capabilities/background-jobs/) for the handler-authoring side.

## Prerequisites

**What you need before tuning**

| Name | Type | Description |
| --- | --- | --- |
| `workers plugin` | `plugins/workers/` | Public install: netscript plugin install @netscript/plugin-workers. Local contributor samples: deno run -A packages/cli/bin/netscript-dev.ts plugin install worker --name workers --samples. The :8091 API enqueues; a separate background process executes. |
| `config/official-plugins/mod.ts` | `defineWorkers(...)` | The generated worker config block — where concurrency, queueProvider, and per-topic scaling live. |
| `@netscript/plugin-workers-core/config` | `import` | defineWorkers, defineJobs, and the WorkersConfig / ScalingConfig / TaskConfig types. |
| `Aspire up (for live runs)` | `cd aspire && aspire start` | The background runner needs Postgres + KV. Aspire injects the worker env vars; see Production pitfalls. |

## Knob 1 — Default concurrency

`concurrency` on the workers config is the size of the Web Worker pool the runner spins up: each slot is its own V8 isolate, so raising it buys parallelism at roughly 20–40 MB per isolate. The schema default is `2`.

Show the type before the call site:

**WorkersConfigData — top-level worker config (@netscript/plugin-workers-core/config)**

| Name | Type | Description |
| --- | --- | --- |
| `concurrency` | `number` | Default worker pool size (V8 isolates running jobs in parallel). Schema default 2. |
| `queueProvider` | `'auto' \| 'deno-kv' \| 'redis' \| 'postgres' \| 'amqp'` | Queue backend. 'auto' resolves one for you. Default 'auto'. |
| `queueName` | `string` | Queue the runner consumes from. Default 'jobs'. |
| `jobsDir / tasksDir` | `string` | Directories scanned for default-exported job and task modules. Defaults ./workers/jobs and ./workers/tasks. |
| `groups` | `WorkerGroupData[]` | Per-topic worker groups, each with its own scaling and retention (Knob 2). |
| `enabled` | `boolean` | Whether workers run at all. Default true. |

```ts
// config/official-plugins/mod.ts
import { defineWorkers } from '@netscript/plugin-workers-core/config';

export const workers = defineWorkers({
  jobsDir: './workers/jobs',
  tasksDir: './workers/tasks',
  queueProvider: 'auto',
  queueName: 'jobs',
  concurrency: 4, // pool size: 4 isolates → ~80-160 MB. Raise for throughput, lower to bound memory.
  enabled: true,
  groups: [],
});
```

## Knob 2 — Per-topic scaling (concurrency + mode)

Different topics deserve different parallelism. A `WorkerGroup` binds a queue `topic` to its own `scaling` policy, so you can run a hot `webhooks` topic at concurrency 10 while a heavy `reports` topic stays at 1. The `mode` selects whether the worker and scheduler share one runner (`combined`) or run as separate processes (`distributed`).

**ScalingConfigData — per-topic scaling (WorkerGroupData.scaling)**

| Name | Type | Description |
| --- | --- | --- |
| `concurrency` | `number` | Max concurrent workers for this topic. Minimum 1, schema default 2. |
| `mode` | `'combined' \| 'distributed'` | Runner deployment: one combined runner vs. distributed runners. Default 'combined'. |

**TopicRetentionConfigData — per-topic execution retention (WorkerGroupData.retention)**

| Name | Type | Description |
| --- | --- | --- |
| `kvDays` | `number` | Days execution history stays in KV. Minimum 1, schema default 7. |
| `dbDays` | `number` | Days execution history stays in the database. Minimum 1, schema default 90. |

```ts
// config/official-plugins/mod.ts
import { defineWorkers } from '@netscript/plugin-workers-core/config';

export const workers = defineWorkers({
  jobsDir: './workers/jobs',
  tasksDir: './workers/tasks',
  queueProvider: 'auto',
  queueName: 'jobs',
  concurrency: 2,
  enabled: true,
  groups: [
    {
      topic: 'webhooks',
      scaling: { mode: 'combined', concurrency: 10 }, // burst-tolerant ingest
      retention: { kvDays: 7, dbDays: 90 },
      jobs: [
        {
          id: 'process-webhook-payload',
          name: 'Process Webhook Payload',
          entrypoint: './process-webhook-payload.ts',
          timeout: 30_000,
          maxRetries: 3,
          tags: ['webhook'],
        },
      ],
    },
  ],
});
```

## Knob 3 — The per-task Deno worker runtime & permissions

A **polyglot task** (`defineTask`, or a `TaskConfig` entry in `workers/runtime/`) runs as a spawned process. For the `deno` runtime, the task's `permissions` object is compiled **directly into the child `deno run` command's `--allow-*` flags** — a `deno` task with no `permissions` set falls back to `--allow-all`, so set them deliberately. The shape mirrors Deno's permission model: each field is `boolean` (grant/deny all) or a `string[]` allowlist.

**WorkerConfigPermissions — per-task Deno grants (compiled to --allow-* flags)**

| Name | Type | Description |
| --- | --- | --- |
| `net` | `boolean \| string[]` | Network access. true → --allow-net; ['api.stripe.com'] → --allow-net=api.stripe.com. |
| `read` | `boolean \| string[]` | Filesystem read. Pass a path allowlist to scope it. |
| `write` | `boolean \| string[]` | Filesystem write. Scope to an output dir in production. |
| `env` | `boolean \| string[]` | Environment variable access. |
| `run` | `boolean \| string[]` | Subprocess access (--allow-run). Needed when the task shells out. |
| `ffi` | `boolean` | Foreign-interface access (--allow-ffi). Leave false unless a native binding is required. |
| `import` | `string[]` | Allowed specifiers for dynamic imports (--allow-import=…). |

**TaskConfig — runtime, timeout & retry knobs (@netscript/plugin-workers-core/config)**

| Name | Type | Description |
| --- | --- | --- |
| `type` | `'deno' \| 'python' \| 'dotnet' \| 'cmd' \| 'powershell' \| 'shell' \| 'executable'` | Runtime that executes the task. Each maps to a runtime adapter. Default 'deno'. |
| `timeout` | `number` | Execution timeout in ms. Schema default 300000 (5 min). |
| `maxRetries` | `number` | Max retry attempts. Schema default 1. |
| `retryDelay` | `number` | Delay between retries in ms. Schema default 1000. |
| `maxConcurrency` | `number` | Max concurrent runs of this one task. Schema default 1. |
| `priority` | `number` | Dispatch priority 0–100. Schema default 50. |
| `permissions` | `WorkerConfigPermissions` | Deno grants for the task (above). Omit on a 'deno' task and it runs with --allow-all. |

The framework also ships named presets so you do not hand-roll the object. `permissions` from `@netscript/plugin-workers-core` exposes `minimal`, `none`, `network`, `filesystem`, `readOnly`, `subprocess`, `full`, and `allAccess`.

Start a new Deno task from the CLI, then tune the generated builder instead of creating its file by hand. The `add-task` verb uses the spaced `add task` shell syntax:

> ns-workers is a shorthand you install once
>
> ns-workers
>
> is a name
>
> you
>
> give the workers plugin's CLI — the scaffold does not create it. Install it once, globally, and every
>
> ns-workers
>
> command on this page works as written:
>
> ```bash
> deno install -gArf -n ns-workers jsr:@netscript/plugin-workers@0.0.1-beta.11/cli
> ```
>
> Rather not install it? Each
>
> ns-workers <verb …>
>
> is exactly
>
> deno x -A jsr:@netscript/plugin-workers@0.0.1-beta.11/cli <verb …>
>
> — run that full form instead.

```sh
ns-workers add task charge-customer --runtime=deno
```

For a non-Deno entrypoint, choose its runtime and path explicitly, for example `ns-workers add task sync-report --runtime=python --entrypoint=scripts/sync-report.py`.

```ts
// workers/tasks/charge-customer.task.ts
import { defineTask } from '@netscript/plugin-workers-core';

// Grant only what the task needs: outbound to Stripe, read its config, nothing else.
export default defineTask('charge-customer')
  .runtime('deno')
  .entrypoint('./charge-customer.ts')
  .timeout(30_000)
  .retry(3)
  .permissions({ net: ['api.stripe.com'], read: ['./config'], env: ['STRIPE_KEY'] })
  .build();
```

```ts
// workers/tasks/sync-report.task.ts
import { defineTask, permissions } from '@netscript/plugin-workers-core';

// 'network' preset = net + env only; spread to tighten one field further.
export default defineTask('sync-report')
  .runtime('deno')
  .entrypoint('./sync-report.ts')
  .timeout(120_000)
  .permissions({ ...permissions.network, read: ['./data'] })
  .build();
```

```json
// workers/runtime/tasks/v1.0.0.json — declarative, no rebuild
{
  "version": "1.0.0",
  "tasks": [
    {
      "id": "charge-customer",
      "name": "Charge Customer",
      "runtime": "deno",
      "entrypoint": "./charge-customer.ts",
      "timeout": 30000,
      "maxRetries": 3,
      "permissions": { "net": ["api.stripe.com"], "env": ["STRIPE_KEY"] }
    }
  ]
}
```

> Jobs have their own timeout/retry defaults
>
> A
>
> job
>
> (
>
> JobConfig
>
> ) and a
>
> task
>
> (
>
> TaskConfig
>
> ) carry the same permission shape but
>
> different
>
> schema defaults: a job defaults to
>
> timeout: 60000
>
> (60s) and
>
> maxRetries: 3
>
> , while a task defaults to
>
> timeout: 300000
>
> (5 min) and
>
> maxRetries: 1
>
> . Set both explicitly in production rather than relying on the default that happens to apply.

## Knob 4 — Runner pool size at the process level

The numbers above configure *definitions*. The actual pool the background process spins up is read from an environment variable when `plugins/workers/bin/combined.ts` (or `worker.ts`) starts. That entrypoint reads **`WORKERS_CONCURRENCY`** (default `1`) and passes it to the Web Worker pool.

```bash
# Override the running pool size for the background worker process.
WORKERS_CONCURRENCY=8 deno run -A plugins/workers/bin/combined.ts
```

> Two concurrency env vars — know which one wins
>
> There are
>
> two
>
> concurrency env names in play and they are
>
> not
>
> the same variable. The worker entrypoint (
>
> plugins/workers/bin/runtime.ts
>
> ) reads
>
> WORKERS_CONCURRENCY
>
> (note the
>
> S
>
> ) and defaults it to
>
> 1
>
> . The Aspire contribution, however, declares and injects
>
> WORKER_CONCURRENCY
>
> (no S, value
>
> 2
>
> ) via its
>
> concurrencyEnvVar
>
> . Under
>
> aspire start
>
> today, the injected
>
> WORKER_CONCURRENCY
>
> does
>
> not
>
> feed the entrypoint's
>
> WORKERS_CONCURRENCY
>
> read, so the process pool falls back to its default. Set
>
> WORKERS_CONCURRENCY
>
> explicitly on the background resource if you need a specific pool size, and treat the per-topic
>
> scaling.concurrency
>
> in
>
> defineWorkers
>
> as the durable, config-driven control. This naming seam is a known rough edge.

## Choosing a runner mode

How a handler is isolated is the `WORKER_RUNTIMES` tunable — three modes, same handler. The scaffold default is the Web Worker isolate path (one V8 isolate per pool slot).

**WORKER_RUNTIMES — runner isolation modes (WorkerRuntime type)**

| Name | Type | Description |
| --- | --- | --- |
| `in-process` | `WorkerRuntime` | Handler runs in the same process. Lowest overhead, no isolation. Best for tests, compiled single-binary deploys, single-tenant local composition. |
| `web-worker` | `WorkerRuntime` | Each pool slot is its own Web Worker / V8 isolate (~20-40 MB). The scaffold default; pool size = concurrency. Keep it low to bound memory. |
| `subprocess` | `WorkerRuntime` | Handler runs in a spawned subprocess. Strongest process isolation; only Deno tasks get permission sandboxing through .permissions(). Python, .NET, shell, PowerShell, and cmd inherit the worker process's OS permissions. |

## In-production pitfalls

> A 'deno' task with no permissions runs --allow-all
>
> The Deno runtime adapter (
>
> buildDenoPermissionFlags
>
> ) returns
>
> --allow-all
>
> when a task's
>
> permissions
>
> object is absent. That is convenient for local scaffolding and dangerous in production: an unpinned task can read your whole disk and reach any host. Always set an explicit
>
> permissions
>
> object (or a tightened preset) on every
>
> deno
>
> task you ship. Path-scope
>
> read
>
> /
>
> write
>
> and host-scope
>
> net
>
> rather than passing bare
>
> true
>
> .

> Concurrency is a memory multiplier
>
> Each
>
> web-worker
>
> slot is a separate V8 isolate at roughly 20–40 MB. A
>
> concurrency
>
> of 20 is up to ~800 MB of isolates before your jobs allocate a byte. Raise concurrency to chase throughput, but profile memory under load and prefer
>
> per-topic
>
> scaling so a hot topic does not starve the rest of the host. Drop to
>
> in-process
>
> for tests and compiled single-binary deploys where isolation is not worth the per-isolate cost.

> Timeouts and retries interact — bound them together
>
> A task with
>
> timeout: 300000
>
> and
>
> maxRetries: 3
>
> can occupy a worker slot for up to ~15 minutes of wall time before it gives up, and each retry waits
>
> retryDelay
>
> first. Under a saturated pool that is real head-of-line blocking. Size
>
> timeout
>
> to the work, keep
>
> maxRetries
>
> small for slow tasks, and pair retries with an
>
> idempotencyKey
>
> on enqueue so a re-delivery after a forced restart does not double-charge. See
>
> Choose a queue provider
>
> for delivery semantics.

## See also

[Background jobs](https://rickylabs.github.io/netscript/netscript/background-processing/workers/)

[Polyglot tasks](https://rickylabs.github.io/netscript/netscript/background-processing/polyglot-tasks/)

[Choose a queue provider](https://rickylabs.github.io/netscript/netscript/data-persistence/how-to/choose-a-queue-provider/)

[Run a polyglot task](https://rickylabs.github.io/netscript/netscript/background-processing/how-to/run-a-polyglot-task/)

[workers](https://rickylabs.github.io/netscript/netscript/reference/workers/)

[Choose a queue provider](https://rickylabs.github.io/netscript/netscript/data-persistence/how-to/choose-a-queue-provider/) [Run a polyglot task](https://rickylabs.github.io/netscript/netscript/background-processing/how-to/run-a-polyglot-task/)
