@startupjs/worker
Run background jobs in StartupJS with a simple API.
This package is useful when work should not block a request lifecycle, for example:
- sending emails or push notifications
- generating reports
- syncing data with external APIs
- recurring background tasks (cron)
- high-priority tasks that should be processed separately
What You Usually Need
After installing @startupjs/worker, most projects only need 2 things:
- Define jobs in
workerJobs/*.js. - Call
await runJob(name, data)from your app code.
The worker plugin is enabled by default, and worker auto-start is enabled by default.
runJob() always waits for completion and returns the job result.
If the job throws, runJob() throws too, so try/catch works as expected.
data can be any JSON-serializable value (object, string, number, array, boolean, null).
Quick Start
1) Install
2) Create a job file
3) Run the job
Fire-and-forget jobs
Use enqueueJob() when the request should return immediately and another part
of your app will track the job status.
jobRef is serializable:
You can wait for it later:
If you need to know whether enqueue created a new job or reused/deduped an
existing one, pass returnMeta: true:
Without returnMeta, enqueueJob() keeps the old behavior and returns only
jobRef.
runJob(name, data, options) is still available and still waits for completion.
Internally it is equivalent to enqueueJob() followed by waitJob().
Job status, logs and counts
Use trackingKey when you need to query jobs by domain entity. Without a
trackingKey, BullMQ can count jobs by queue state, but it can not efficiently
answer domain queries like "active voiceover jobs for this course".
getJobsCount({ name }) also requires trackingKey; otherwise the result would
be a capped scan instead of an exact count.
Delayed jobs
Use either delay or startAt, not both.
Per-call singleton and queue selection
Job files can still export singleton, but you can also set it per enqueue call:
By default duplicates return the existing in-flight job ref. For per-call singleton you can choose a different duplicate policy:
You can override the queue at runtime:
Debounce and throttle
Debounce and throttle are exposed through a package-level API. Basic debounce and leading throttle are implemented with BullMQ deduplication. Trailing throttle keeps the first job immediate and schedules one latest-value job at the end of the throttle window.
For debounce, debounce.delay is also used as the BullMQ job delay, so do not
pass a separate delay or startAt option.
For debounce duplicates, the default policy is replace, so the latest delayed
payload wins. You can opt out with replace: false or choose an explicit
onDuplicate policy:
Production Deployment (Important)
Default behavior:
- app server process also runs workers
- this is fine for local/dev and simple deployments
Production recommendation for larger setups:
- run a dedicated worker microservice with
npx startupjs start-worker-production - disable worker auto-start in your app server production process
Use autoStartProduction: false in app server config:
Then run workers separately:
This command starts workers even in production mode, because it initializes workers directly.
Cron Jobs
Cron jobs are for tasks that must run on a schedule (for example cleanup, sync, or periodic reports).
Use cron when:
- the job should run automatically at fixed times
- you do not want to trigger it manually from app code each time
Important:
- cron does not replace manual runs
- you can still run the same job anytime with
runJob(...) cron.datais the input payload passed to the job for scheduled runs
API:
export const cron = '<cron pattern>'- or
export const cron = { pattern: '<cron pattern>', data: <any serializable value> }
Example without data:
Example with data:
The scheduled run above is equivalent to calling:
Singleton Jobs
Singleton jobs prevent duplicate work.
Use singleton when:
- the same job should not run multiple times in parallel
- duplicate requests can happen and should collapse into one execution
There are two singleton modes:
- global singleton: one in-flight job for that job name
- keyed singleton: one in-flight job per key
What keyed singleton means:
- you return a key from
singleton(data) - jobs with the same key are treated as duplicates
- jobs with different keys can still run in parallel
Why keyed singleton is useful:
- avoid duplicate work for one entity (user, org, document)
- still keep concurrency across different entities
API:
export const singleton = truefor one global singleton per job nameexport const singleton = (data) => valuefor keyed singleton (for example per user)
Global singleton example:
Keyed singleton example (per user):
In this example:
rebuildUserFeed({ userId: 'u1' })+rebuildUserFeed({ userId: 'u1' })collapse into one executionrebuildUserFeed({ userId: 'u1' })andrebuildUserFeed({ userId: 'u2' })can run in parallel
In keyed mode, the returned value must be JSON-serializable.
Job File API
Each file in workerJobs should export:
default: required async/sync functioncron: optional cron configworker: optional worker name (defaultorpriority)singleton: optional deduplication config
Job Context (default export second argument)
Your handler receives:
log(message, { data, err })log.warn(message, { data, err })log.error(message, { data, err })job(raw queue job object)jobRef(serializable job reference)progress(value)(updates BullMQ job progress)enqueueJob(name, data, options)waitJob(jobRef, options)getJobStatus(jobRef)backendcreateModel()
Use log(...) instead of console.log(...) when you want logs visible in the queue dashboard.
When the worker backend is initialized, handlers can use model helpers:
createModel() throws if worker backend initialization is disabled with
ensureBackend: false.
Worker Lifecycle Events
Use events in worker server config when you need a generic integration point
for metrics, domain status rows, notifications or custom tracking.
Each event contains:
ref: serializable job refname: job nameworker: queue namedata: job payload datameta: worker metadatastatejob: raw BullMQ job object
onCompleted also receives result, onFailed receives error, and
onProgress receives progress.
Lifecycle hooks are managed best-effort:
- hook errors are logged and do not change BullMQ job outcome
onStarted/onProgress/onCompleted/onFailedrun from parent BullMQWorkerevents- worker shutdown waits for currently pending hook promises before closing runtime resources
Use BullMQ job state as the source of truth and lifecycle hooks for metrics, notifications or eventually consistent read models.
Examples By Use Case
1) Simple async background task
Why: move slow work out of request handlers.
2) Cron job
Why: run periodic maintenance tasks automatically.
With data payload:
3) Priority queue job
Why: keep urgent tasks separate from regular background load.
4) Singleton job (global)
Why: prevent duplicate executions of the same job type.
5) Singleton job by key (for example userId)
Why: allow parallel work for different users, but dedupe per user.
6) Per-call timeout override
Why: most jobs use a global timeout, but some calls need a custom value.
7) Add jobs from another plugin
Why: reusable modules can contribute their own jobs.
Startup and Configuration
Server-side worker options:
Option meanings:
autoStart(trueby default): auto-initialize workers on server startup.autoStartProduction(trueby default): iffalse, workers do not auto-start whenNODE_ENV=production.concurrency(300): per-worker concurrency.ensureBackend(true): initialize StartupJS backend before running a job. Set tofalseonly for pure BullMQ jobs/tests that do not use model/backend APIs.jobTimeout(30000ms): default timeout for jobs.queuePrefix(redisPrefix): BullMQ Redis key prefix. Mostly useful for isolated tests or dedicated deployments.useSeparateProcess(false): execute job handlers in sandboxed child runner.useWorkerThreads(true): when sandbox is on, use worker threads.dashboard: queue UI settings. Providerouteto enable the dashboard route.
Worker Topology (default + priority)
The package has two fixed worker names:
defaultpriority
By default both start.
To start only specific workers, use WORKERS env var (comma-separated):
This enables setups like:
- app servers enqueue jobs
- one worker service handles both queues
- another dedicated worker service handles only
priority
Common commands:
If you run dedicated worker services in all environments, set autoStart: false.
If you split only in production, set autoStartProduction: false so dev/stage behavior stays unchanged.
Public Exports
@startupjs/worker:runJob(name, data?, options?)@startupjs/worker: named job helpers listed above@startupjs/worker/init:initWorker(options?)@startupjs/worker/plugin: StartupJS plugin export used by plugin registry
Error Handling and Results
runJobreturns the value returned by your job handler.- If your job throws,
runJobthrows. - If the job does not exist,
runJobthrows with a list of available jobs. - If a job declares unknown
worker, initialization fails and lists supported workers.
Timeout Behavior
- Timeouts are enforced as promise timeouts for all modes.
- In same-process mode, timed-out code cannot be force-killed; execution may continue in background.
- In separate-process mode, timeout can terminate the sandbox runner for harder isolation.
Technical Details (Optional)
Under the hood this package uses BullMQ, but you usually do not need to care.
Implementation summary:
- Jobs are discovered from
workerJobs/*.js. - Additional jobs can be injected through StartupJS
workerJobsplugin hook. initWorkerstarts workers, syncs cron schedulers, and removes stale schedulers.runJobenqueues a job and waits for completion before returning.- Queue names are fixed to
defaultandpriority.