Skip to main content
0.0.x

Tune the worker runtime

Tune queue concurrency, per-topic deployment mode, per-task Deno permissions, and timeouts/retries without changing a job handler.

Jobs currently execute in-process in the background worker service. Concurrency overlaps asynchronous I/O; it does not create Web Worker isolates or parallelize synchronous CPU work. These settings live in config/official-plugins/mod.ts (defineWorkers(...)), per-task runtime config, or an environment variable read by the worker entrypoint. See Background jobs for handler authoring.

Prerequisites

What you need before tuning
NameTypeDescription
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 workers API service (on its assigned port) 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 declares queue concurrency (schema default 2). Handlers share the background worker's isolate. Increase concurrency for I/O-bound work, then measure throughput and memory under load; there is no per-slot isolate allocation.

Show the type before the call site:

WorkersConfigData — top-level worker config (@netscript/plugin-workers-core/config)
NameTypeDescription
concurrency number Default queue concurrency for in-process jobs. Schema default 2; no isolate pool is allocated.
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, // Queue concurrency: overlap up to 4 I/O-bound jobs; no CPU parallelism.
  enabled: true,
  groups: [],
});

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

Different topics deserve different concurrency budgets. 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.

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 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.

// 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 — Queue concurrency at the process level

The numbers above configure definitions. The background entrypoint (plugins/workers/bin/combined.ts or worker.ts) reads WORKERS_CONCURRENCY (default 1) and passes it as queue concurrency. The in-process runner ignores the legacy poolSize and workerUrl options; it does not allocate an isolate pool.

# Override the queue concurrency for the background worker process.
WORKERS_CONCURRENCY=8 deno run -A plugins/workers/bin/combined.ts

Under aspire start you do not set it by hand: the workers plugin's Aspire metadata injects WORKERS_CONCURRENCY on the background resource with the plugin's declared default (2), and the deploy-generated .env writes the same variable. Change the declared value in appsettings.json (the background processor's Concurrency) and regenerate the AppHost helpers with netscript service generate, or override WORKERS_CONCURRENCY in the environment.

Choosing a runner mode

Only in-process job execution is implemented. WORKER_RUNTIMES / WorkerRuntime are compatibility vocabulary, not a selectable runtime setting. The web-worker and subprocess job-runner modes are reserved. Polyglot task executors can spawn subprocesses; that does not enable subprocess isolation for job handlers.

WORKER_RUNTIMES — implemented and reserved job-runner vocabulary
NameTypeDescription
in-process WorkerRuntime Implemented: handlers share the background worker process and event loop; asynchronous I/O can overlap, synchronous CPU work cannot run in parallel.
web-worker WorkerRuntime Reserved job-runner vocabulary; no Web Worker isolate pool is implemented.
subprocess WorkerRuntime Reserved job-runner vocabulary. Subprocesses are implemented for task executors, not job handlers; Deno task permissions apply only to Deno tasks.

Abort cleanup budget

InProcessJobRunner accepts abortGracePeriodMs in its constructor options, with a default of 1,000 ms. After timeout, shutdown, or caller cancellation, the runner aborts the handler signal immediately and waits up to this budget for handler/progress cleanup. If cleanup does not settle, dispatch rejects with the abort reason. Returning success after abort still rejects.

This budget bounds waiting; it cannot physically terminate in-process JavaScript. A handler that ignores its signal can continue side effects after dispatch rejects, and a blocking synchronous loop prevents timers from firing. Use abort-aware I/O, yielding checkpoints, and finally cleanup. Do not describe this setting as a forced worker or subprocess termination delay, or as a top-level defineWorkers configuration field.

See JobContext cancellation for deadlineAt selection and reason names. The shutdown manager's overall timeout is a separate host drain budget; size both within your platform's shutdown window.

In-production pitfalls

See also

Background jobs

Polyglot tasks

Choose a queue provider

Run a polyglot task

workers