Skip to main content

Run a Worker - TypeScript SDK

View Markdown

This page covers long-lived Workers that you host and run as persistent processes. For Workers that run on serverless compute like AWS Lambda, see Serverless Workers.

Create and run a Worker

A Worker Process is where Workflow Functions and Activity Functions execute. Each Worker polls exactly one Task Queue, and every Worker polling a given Task Queue must register the same Workflow Types and Activity Types.

Create a Worker with Worker.create(), passing a connection, the Task Queue to poll, and the Workflows and Activities it can execute. Call run() to start polling.

hello-world/src/worker.ts

import { NativeConnection, Worker } from '@temporalio/worker';
import * as activities from './activities';

async function run() {
// Step 1: Establish a connection with Temporal server.
//
// Worker code uses `@temporalio/worker.NativeConnection`.
// (But in your application code it's `@temporalio/client.Connection`.)
const connection = await NativeConnection.connect({
address: 'localhost:7233',
// TLS and gRPC metadata configuration goes here.
});
try {
// Step 2: Register Workflows and Activities with the Worker.
const worker = await Worker.create({
connection,
namespace: 'default',
taskQueue: 'hello-world',
// Workflows are registered using a path as they run in a separate JS context.
workflowsPath: require.resolve('./workflows'),
activities,
});

// Step 3: Start accepting tasks on the `hello-world` queue
//
// The worker runs until it encounters an unexpected error or the process receives a shutdown signal registered on
// the SDK Runtime object.
//
// By default, worker logs are written via the Runtime logger to STDERR at INFO level.
//
// See https://typescript.temporal.io/api/classes/worker.Runtime#install to customize these defaults.
await worker.run();
} finally {
// Close the connection once the worker has stopped
await connection.close();
}
}

run().catch((err) => {
console.error(err);
process.exit(1);
});

Workflows are registered by path rather than by value, because they run in a separate JavaScript context.

Register Workflows and Activities

All Workers listening to the same Task Queue must be registered to handle the same Workflow Types and Activity Types. If a Worker polls a Task for a type it does not know about, the Task fails. The Workflow Execution itself does not fail.

In development, use workflowsPath:

snippets/src/worker.ts

import { Worker } from '@temporalio/worker';
import * as activities from './activities';

async function run() {
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
taskQueue: 'snippets',
activities,
});

await worker.run();
}

In this snippet, the Worker bundles the Workflow code at runtime.

In production, you can improve your Worker's startup time by bundling in advance. As part of your production build, call bundleWorkflowCode:

production/src/scripts/build-workflow-bundle.ts

import { bundleWorkflowCode } from '@temporalio/worker';
import { writeFile } from 'fs/promises';
import path from 'path';

async function bundle() {
const { code } = await bundleWorkflowCode({
workflowsPath: require.resolve('../workflows'),
});
const codePath = path.join(__dirname, '../../workflow-bundle.js');

await writeFile(codePath, code);
console.log(`Bundle written to ${codePath}`);
}

Then pass the bundle to the Worker:

production/src/worker.ts

const workflowOption = () =>
process.env.NODE_ENV === 'production'
? {
workflowBundle: {
codePath: require.resolve('../workflow-bundle.js'),
},
}
: { workflowsPath: require.resolve('./workflows') };

async function run() {
const worker = await Worker.create({
...workflowOption(),
activities,
taskQueue: 'production-sample',
});

await worker.run();
}

Connect to Temporal Cloud

To run a Worker against Temporal Cloud, configure the connection with your Namespace address and authentication credentials. See Connect to Temporal Cloud for setup instructions.

Configure Worker options

Worker.create() accepts options that control concurrency limits, pollers, timeouts, and caching. The defaults work for most cases.

To tune these values against real load, see Worker performance and the Worker tuning reference.

Run a versioned Worker

Set a Worker Deployment Version and enable versioning in workerDeploymentOptions. When useWorkerVersioning is true, defaultVersioningBehavior is required.

features/snippets/worker/worker.ts

const worker = await Worker.create({
connection,
taskQueue: 'my-task-queue',
workflowsPath: require.resolve('./workflows'),
workerDeploymentOptions: {
version: { deploymentName: 'my-app', buildId: '1.0' },
useWorkerVersioning: true,
defaultVersioningBehavior: 'PINNED',
},
});

See Worker Versioning for the available versioning behaviors and how new versions roll out.

Shut down a Worker

Workers shut down if they receive any of the Signals enumerated in shutdownSignals: 'SIGINT', 'SIGTERM', 'SIGQUIT', and 'SIGUSR2'.

In development, shut down Workers with Ctrl+C (SIGINT) or nodemon (SIGUSR2). In production, give Workers time to finish in-progress Activities by setting shutdownGraceTime:

features/snippets/worker/worker.ts

const worker = await Worker.create({
connection,
taskQueue: 'my-task-queue',
workflowsPath: require.resolve('./workflows'),
shutdownGraceTime: '30s',
});

await worker.run();

As soon as a Worker receives a shutdown Signal or request, the Worker stops polling for new Tasks and allows in-flight Tasks to complete until shutdownGraceTime is reached. Any Activities still running at that time stop running and are rescheduled by the Temporal Service when an Activity timeout occurs.

If you must guarantee that the Worker eventually shuts down, set shutdownForceTime.

You might want to programmatically shut down Workers (with Worker.shutdown()) in integration tests or when automating a fleet of Workers.

See Worker shutdown for what happens to in-flight Workflow Tasks and Activities.

Worker states

At any time, you can Query Worker state with Worker.getState(). A Worker is always in one of seven states:

  • INITIALIZED: The initial state of the Worker after calling Worker.create() and successfully connecting to the server.
  • RUNNING: Worker.run() was called and the Worker is polling Task Queues.
  • FAILED: The Worker encountered an unrecoverable error; Worker.run() should reject with the error.
  • The last four states are related to the Worker shutdown process:
    • STOPPING: The Worker received a shutdown Signal or Worker.shutdown() was called. The Worker will forcefully shut down after shutdownGraceTime expires.
    • DRAINING: All Workflow Tasks have been drained; waiting for Activities and cached Workflows eviction.
    • DRAINED: All Activities and Workflows have completed; ready to shut down.
    • STOPPED: Shutdown complete; worker.run() resolves.

If you need more visibility into internal Worker state, see the Worker class in the API reference.

Run a Worker on Docker

info

To improve Worker startup time, prepare Workflow bundles ahead of time. See the production sample for details.

Workers based on the TypeScript SDK can be deployed and run as Docker containers.

Use an LTS Node.js release such as 18, 20, 22, or 24. Both amd64 and arm64 architectures are supported. A glibc-based image is required; musl-based images are not supported (see below).

The most direct way to deploy a TypeScript SDK Worker on Docker is to start with the node:20-bullseye image. For example:

FROM node:20-bullseye

# For better cache utilization, copy package.json and lock file first and install the dependencies before copying the
# rest of the application and building.
COPY . /app
WORKDIR /app

# Alternatively, run npm ci, which installs only dependencies specified in the lock file and is generally faster.
RUN npm install --only=production \
&& npm run build

CMD ["npm", "start"]

For smaller images and/or more secure deployments, it is also possible to use -slim Docker image variants (like node:20-bullseye-slim) or distroless/nodejs Docker images (like gcr.io/distroless/nodejs20-debian11) with the following caveats.

Using node:slim images

node:slim images do not contain some of the common packages found in regular images. This results in significantly smaller images.

However, TypeScript SDK requires the presence of root TLS certificates (the ca-certificates package), which are not included in slim images. The ca-certificates package is required even when connecting to a local Temporal Service or when using a server connection config that doesn't explicitly use TLS.

For this reason, the ca-certificates package must be installed during the construction of the Docker image. For example:

FROM node:20-bullseye-slim

RUN apt-get update \
&& apt-get install -y ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# ... same as with regular image

Failure to install this dependency results in a [TransportError: transport error] runtime error, because the certificates cannot be verified.

Using distroless/nodejs images

distroless/nodejs images include only the files that are strictly required to execute node. This results in even smaller images (approximately half the size of node:slim images). It also significantly reduces the surface of potential security issues that could be exploited by a hacker in the resulting Docker images.

It is generally possible and safe to execute TypeScript SDK Workers using distroless/nodejs images (unless your code itself requires dependencies that are not included in distroless/nodejs).

However, some tools required for the build process (notably the npm command) are not included in the distroless/nodejs image. This might result in various error messages during the Docker build.

The recommended solution is to use a multi-step Dockerfile. For example:

# -- BUILD STEP --

FROM node:20-bullseye AS builder

COPY . /app
WORKDIR /app

RUN npm install --only=production \
&& npm run build

# -- RESULTING IMAGE --

FROM gcr.io/distroless/nodejs20-debian11

COPY --from=builder /app /app
WORKDIR /app

CMD ["node", "build/worker.js"]

Properly configure Node.js memory in Docker

By default, node configures its maximum old-gen memory to 25% of the physical memory of the machine on which it is executing, with a maximum of 4 GB. This is likely inappropriate when running Node.js in a Docker environment and can result in either underusage of available memory (node only uses a fraction of the memory allocated to the container) or overusage (node tries to use more memory than what is allocated to the container, which will eventually lead to the process being killed by the operating system).

Therefore we recommended that you always explicitly set the --max-old-space-size node argument to approximately 80% of the maximum size (in megabytes) that you want to allocate the node process. You might need some experimentation and adjustment to find the most appropriate value based on your specific application.

In practice, it is generally easier to provide this argument through the NODE_OPTIONS environment variable.

Do not use Alpine

Alpine replaces glibc with musl, which is incompatible with the Rust core of the TypeScript SDK. If you receive errors like the following, it's probably because you are using Alpine.

Error: Error loading shared library ld-linux-x86-64.so.2: No such file or directory (needed by /opt/app/node_modules/@temporalio/core-bridge/index.node)

Or like this:

Error: Error relocating /opt/app/node_modules/@temporalio/core-bridge/index.node: __register_atfork: symbol not found