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 for the handler-authoring side.
Prerequisites
| Name | Type | Description |
|---|---|---|
workers plugin |
plugins/workers/ |
Public install: netscript plugin add @netscript/plugin-workers. Local contributor samples: deno run -A packages/cli/bin/netscript-dev.ts plugin add 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:
| 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. |
// 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).
| 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'. |
| 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. |
// 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.
| 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=…). |
| 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.
// 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();
// 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();
// 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"] }
}
]
}
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.
# Override the running pool size for the background worker process.
WORKERS_CONCURRENCY=8 deno run -A plugins/workers/bin/combined.ts
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).
| 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. |