Skip to main content

TypeScript worker runtime

The @zeridion/flare/worker subpath is a complete background-worker host. You define jobs with a small factory API, hand them to a FlareWorker, and the runtime registers with Flare, long-polls for work, dispatches each job to your handler with bounded concurrency, reports liveness and progress, honours server-side cancellation, acknowledges each outcome, and drains in-flight work on shutdown.

The worker is opt-in by import — the thin client at @zeridion/flare stays dependency-free and edge-safe; importing @zeridion/flare/worker pulls in the runtime only where you need it.

npm install @zeridion/flare

Runtime: Node 20.3+ (the worker uses AbortSignal.any / AbortSignal.timeout). The thin client remains edge/browser-safe.

Quick start

import { FlareWorker, defineJob, defineRecurringJob } from "@zeridion/flare/worker";

const sendWelcome = defineJob<{ email: string; name: string }>({
jobType: "SendWelcomeEmail",
queue: "default",
maxAttempts: 5,
timeoutSeconds: 60,
async handle(payload, ctx) {
ctx.reportProgress(0.5);
await mailer.send(payload.email, { signal: ctx.signal });
ctx.reportProgress(1);
},
});

const nightly = defineRecurringJob({
jobType: "NightlyCleanup",
cron: "0 3 * * *",
timezone: "UTC",
async handle(ctx) {
await db.sessions.deleteExpired({ signal: ctx.signal });
},
});

const worker = new FlareWorker({
apiKey: process.env.FLARE_API_KEY,
concurrency: 5,
jobs: [sendWelcome, nightly],
});

await worker.run(); // starts polling + wires SIGTERM/SIGINT; resolves once drained

What the runtime does for you

  1. Registers the worker, its queues, job types, and any recurring schedules on startup. Registration is best-effort — a failure is logged and polling continues.
  2. Long-polls for jobs, reporting how many free slots it has so the server never hands it more work than it can run.
  3. Dispatches each job to the matching handler, bounded by your concurrency limit.
  4. Heartbeats each in-flight job on a cadence derived from the job's timeout, carrying the latest reported progress. A heartbeat that returns a cancel signal aborts the job.
  5. Acknowledges every outcome. A successful return acks the job succeeded; any thrown error (including a timeout or a server cancel) acks it failed. The server decides whether to retry — the worker only reports.
  6. Drains on shutdown: it stops polling, lets in-flight jobs finish within a grace window, and resolves.

Lifecycle methods

MethodBehaviour
start()Begin polling in the background. Returns immediately. No-ops when no jobs are registered. Does not install signal handlers.
run()start() plus SIGTERM/SIGINT wiring for a graceful stop. Resolves once the worker has fully drained. Use this for a standalone worker process.
stop()Stop polling and drain in-flight jobs within the grace window. Idempotent.
waitUntilStopped()Resolves once the worker has fully stopped.

workerId (read-only) is the identifier the worker registers under, of the form wrk_{host}_{pid}_{random}. The host segment is sanitized to the characters the server accepts, and the random segment comes from a cryptographic source so two workers — even in containers that share a hostname — never collide.

Cancellation is cooperative

A job is cancelled by aborting ctx.signal — on timeout, on host shutdown, or when the server requests it. Cancellation only takes effect when your handler observes the signal. Pass ctx.signal to every async call you make (fetch(url, { signal: ctx.signal }), database queries, and so on) and check ctx.signal.aborted in long loops. A handler that ignores the signal cannot be forced to stop; it runs until it returns on its own.

Because a busy event loop can starve the heartbeat that keeps a job alive, offload CPU-heavy work or await-yield periodically so heartbeats keep their cadence.

At-least-once delivery

Acknowledgement is best-effort, so a job can be delivered more than once — for example, if the process crashes after the work completes but before the acknowledgement lands. Write idempotent handlers. Guard side effects with a dedup check keyed on ctx.jobId:

async handle(payload, ctx) {
if (await alreadyProcessed(ctx.jobId)) return;
await doWork(payload);
await markProcessed(ctx.jobId);
}

Reference

PageWhat it covers
Defining jobsdefineJob, payload typing and validation, the handler contract.
Recurring jobsdefineRecurringJob, cron expressions, and timezones.
Job contextThe JobContext handed to every handler: metadata, signal, progress, logging.
ConfigurationFlareWorker options — concurrency, polling, drain, logging.

See also