Skip to main content
Alpha

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

What you need before tuning
NameTypeDescription
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:

WorkersConfigData — top-level worker config (@netscript/plugin-workers-core/config)
NameTypeDescription
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).

ScalingConfigData — per-topic scaling (WorkerGroupData.scaling)
NameTypeDescription
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)
NameTypeDescription
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.

WorkerConfigPermissions — per-task Deno grants (compiled to --allow-* flags)
NameTypeDescription
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)
NameTypeDescription
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).

WORKER_RUNTIMES — runner isolation modes (WorkerRuntime type)
NameTypeDescription
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

See also

Background jobs

Polyglot tasks

Choose a queue provider

Run a polyglot task

workers