Skip to content

Queue & Background Workers ​

The Queue system lets you defer time-consuming tasks - sending emails, generating reports, processing uploads - to background workers so they don't block HTTP responses.


Configuration ​

Environment variable ​

ini
QUEUE_CONNECTION=sync

config/queue.ts ​

Four named connections are provided by default:

typescript
import type { QueueConfig } from "struxjs";
import { env } from "struxjs";

const queueConfig: QueueConfig = {
    default: env("QUEUE_CONNECTION", "sync"),

    connections: {
        sync: {
            driver: "sync",
        },
        redis: {
            driver: "redis",
            connection: "default",  // matches a key in config/redis.ts
            queue: "default",
        },
        database: {
            driver: "database",
            table: "jobs",
            failedTable: "failed_jobs",
            reservationTimeout: 90, // seconds before a stuck job is released
            queue: "default",
        },
        file: {
            driver: "file",
            storagePath: undefined, // defaults to storage/framework/queue
            queue: "default",
        },
    },
};

export default queueConfig;

Available Drivers ​

sync ​

Executes jobs immediately in the same process - no worker needed. Useful for local development and testing where you don't want to run a separate worker process.

ini
QUEUE_CONNECTION=sync

redis ​

Stores jobs in Redis lists. Immediate jobs use LPUSH/BRPOP. Delayed jobs are placed in a sorted set (ZADD) keyed by their run timestamp and moved to the main list when they become ready.

ini
QUEUE_CONNECTION=redis

Redis connection parameters are read from config/redis.ts (default connection by default).

database ​

Stores jobs in a SQL table. Supports race-condition-safe job reservation and a configurable reservation timeout - if a worker crashes mid-job, the reservation expires and another worker can pick it up.

ini
QUEUE_CONNECTION=database

Generate the required migrations:

bash
npx strux queue:table
npx strux migrate

This creates two tables:

jobs - pending and in-flight jobs:

typescript
table.string("id", 36).primary();
table.string("queue", 255).notNullable().defaultTo("default").index();
table.text("payload").notNullable();
table.integer("attempts").notNullable().defaultTo(0);
table.bigInteger("available_at").notNullable().index();
table.bigInteger("reserved_at").nullable().defaultTo(null);
table.bigInteger("created_at").notNullable();

failed_jobs - permanently failed jobs:

typescript
table.string("id", 36).primary();
table.string("queue", 255).notNullable().index();
table.text("payload").notNullable();
table.bigInteger("failed_at").notNullable().index();

file ​

Stores each job as a JSON file in storage/framework/queue/<queue-name>/. Failed jobs go into storage/framework/queue/failed/. No external dependencies required.

ini
QUEUE_CONNECTION=file

Creating a Job ​

bash
npx strux make:job SendEmailJob

Generated file (app/Jobs/SendEmailJob.ts):

typescript
import { Job } from "struxjs";

export class SendEmailJob extends Job {
    public queue = "default";
    public tries = 3;

    constructor(
        public readonly email: string,
        public readonly subject: string
    ) {
        super();
    }

    public async handle(): Promise<void> {
        // Job logic here
        await mailer.send(this.email, this.subject);
    }

    public async failed(error: Error): Promise<void> {
        // Called when all retry attempts are exhausted
        console.error("SendEmailJob permanently failed:", error.message);
    }
}

Job properties ​

PropertyTypeDefaultDescription
queuestring"default"Queue name this job belongs to
triesnumber3Max retry attempts before moving to failed jobs
delaynumber0Seconds before the job becomes available
timeoutnumber60Max seconds a single handle() execution may run

handle() ​

Contains the job's work logic. If it throws, the job is retried until tries is exhausted.

failed(error) ​

Called once when all retries are exhausted. Use it to send alerts, clean up resources, or log permanently failed work. The default implementation is a no-op.


Job Auto-Discovery ​

StruxJS fully automates job registration out of the box.

When your application starts (both web server and CLI workers), the core engine automatically scans the app/Jobs/ directory and registers any exported Job classes.

Internal framework jobs (such as SendMailJob used by the Mail component) are also auto-registered safely behind the scenes.


Dispatching Jobs ​

dispatch(job, options?) ​

The global dispatch helper is the recommended way to push a job to the queue:

typescript
import { dispatch } from "struxjs";
import { SendEmailJob } from "../Jobs/SendEmailJob.js";

// Dispatch to default queue
await dispatch(new SendEmailJob("alice@example.com", "Welcome!"));

// Dispatch to a named queue
await dispatch(new SendEmailJob("alice@example.com", "Welcome!"), {
    queue: "emails",
});

// Dispatch with a delay (run after 60 seconds)
await dispatch(new SendEmailJob("alice@example.com", "Welcome!"), {
    delay: 60,
});

// Dispatch to a specific connection
await dispatch(new SendEmailJob("alice@example.com", "Welcome!"), {
    connection: "redis",
});

Queue.push(job, options?) ​

Same as dispatch() - dispatch is a thin alias over Queue.push:

typescript
import { Queue } from "struxjs";

await Queue.push(new SendEmailJob("alice@example.com", "Welcome!"), {
    queue: "emails",
    delay: 30,
});

Queue.later(delay, job, options?) ​

Syntactic sugar for dispatching a delayed job:

typescript
// Run after 5 minutes
await Queue.later(300, new SendEmailJob("alice@example.com", "Welcome!"));

Per-job queue and delay ​

You can set defaults directly on the job class so you don't have to pass options every time:

typescript
export class SendEmailJob extends Job {
    public queue = "emails";  // always goes to the emails queue
    public delay = 10;        // always delayed by 10 seconds
    public tries = 5;
}

await dispatch(new SendEmailJob("alice@example.com", "Welcome!"));

Options passed to dispatch() override the job's own properties.


Running the Worker ​

Start a worker process with queue:work:

bash
npx strux queue:work

The worker polls the default queue in a loop, processing one job at a time.

Worker options ​

bash
npx strux queue:work \
  --queue emails \
  --connection redis \
  --tries 5 \
  --timeout 120 \
  --sleep 5 \
  --stop-when-empty
OptionDefaultDescription
-q, --queue"default"Queue name to consume
-c, --connectionconfig defaultNamed connection from config/queue.ts
--tries3Max retry attempts per job
--timeout60Per-job execution timeout in seconds
--sleep3Seconds to wait when queue is empty before polling again
--stop-when-emptyfalseExit the worker when queue is empty instead of polling forever

Multiple queues ​

Run separate worker processes for different queues:

bash
# Terminal 1 - high-priority jobs
npx strux queue:work --queue critical --tries 5

# Terminal 2 - email jobs
npx strux queue:work --queue emails --timeout 30

# Terminal 3 - low-priority background jobs
npx strux queue:work --queue default

Graceful shutdown ​

The worker listens for SIGTERM and SIGINT. When received, it finishes processing the current job and then stops cleanly:

bash
kill -SIGTERM <worker-pid>

Failed Jobs ​

When a job exhausts all retry attempts, it is moved to the failed jobs store. The failed() method on the job is called before it is stored.

List failed jobs ​

bash
npx strux queue:failed

# Filter by queue
npx strux queue:failed --queue emails

Output:

[StruxJS Queue]: 2 failed job(s):

  ID:       job_17200000_abc123
  Class:    SendEmailJob
  Queue:    emails
  Attempts: 3
  Failed:   2026-07-28T10:45:00.000Z
  Error:    Connection refused

Retry a failed job ​

bash
# Retry a specific job by ID
npx strux queue:retry job_17200000_abc123

# Retry all failed jobs
npx strux queue:retry all

Flush all failed jobs ​

bash
npx strux queue:flush

Queue Size ​

Check the number of pending jobs programmatically:

typescript
import { Queue } from "struxjs";

const count = await Queue.size("emails");
const defaultCount = await Queue.size(); // defaults to "default"

CLI Reference ​

CommandDescription
npx strux make:job <name>Create a new Job class
npx strux queue:workStart a worker on the default queue
npx strux queue:work --queue <name>Start a worker on a named queue
npx strux queue:failedList all permanently failed jobs
npx strux queue:retry <id>Re-queue a specific failed job
npx strux queue:retry allRe-queue all failed jobs
npx strux queue:flushDelete all failed jobs
npx strux queue:tableGenerate migrations for the database driver
npx strux queue:table --jobs-table=<name>Custom jobs table name (default: jobs)
npx strux queue:table --failed-table=<name>Custom failed jobs table name (default: failed_jobs)

Driver Comparison ​

DriverPersistenceDelayed jobsShared across serversBest for
syncNoNoNoDev, testing
fileYes (disk)YesNoSingle-server, no infrastructure
databaseYes (SQL)YesYesMulti-server, no Redis
redisYes (Redis)YesYesProduction, high-throughput

Production Setup ​

In production you need one or more persistent worker processes running alongside your web server. Unlike the web server, queue workers are long-running Node.js processes - use a process manager to keep them alive.

Using PM2 ​

Install PM2 globally and create an ecosystem config:

bash
npm install -g pm2

ecosystem.config.cjs:

javascript
module.exports = {
    apps: [
        // Web server
        {
            name: "web",
            script: "node",
            args: "dist/bootstrap.js",
            instances: 1,
            autorestart: true,
            watch: false,
        },
        // Default queue worker
        {
            name: "worker-default",
            script: "node",
            args: "dist/cli/index.js queue:work --queue default --tries 3 --timeout 60",
            instances: 1,
            autorestart: true,
            watch: false,
        },
        // Emails queue - separate worker with shorter timeout
        {
            name: "worker-emails",
            script: "node",
            args: "dist/cli/index.js queue:work --queue emails --tries 5 --timeout 30",
            instances: 1,
            autorestart: true,
            watch: false,
        },
    ],
};

Start all processes:

bash
pm2 start ecosystem.config.cjs
pm2 save
pm2 startup

Using systemd ​

Create a service file at /etc/systemd/system/strux-worker.service:

ini
[Unit]
Description=StruxJS Queue Worker
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/myapp
ExecStart=node dist/cli/index.js queue:work --queue default --tries 3 --timeout 60
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Enable and start:

bash
sudo systemctl enable strux-worker
sudo systemctl start strux-worker
sudo systemctl status strux-worker

For multiple queues, create separate service files (strux-worker-emails.service, etc.) with different --queue arguments.

Use the redis driver in production. It supports delayed jobs natively, handles high throughput without locking overhead, and scales cleanly across multiple servers and workers:

ini
QUEUE_CONNECTION=redis

Ensure config/redis.ts points to your production Redis instance via REDIS_HOST, REDIS_PORT, and REDIS_PASSWORD.


Troubleshooting ​

"Container not booted - using SyncDriver as fallback" ​

This warning appears when the queue worker can't load the application configuration. The worker is trying to use config/queue.ts but the container hasn't been bootstrapped.

Solution: Ensure bootstrap-cli.ts exists and contains:

typescript
import { Application, Queue, Cache, Mail, Job, SendMailJob, BaseModel } from "struxjs";
import { SendEmailJob } from "./app/Jobs/SendEmailJob.js";
import dotenv from "dotenv";
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname  = path.dirname(__filename);

dotenv.config({ path: path.join(__dirname, ".env") });

const app = new Application(__dirname);
await app.bootstrap();

const dbConfig = app.container.make("config.database");
BaseModel.bootConnection(dbConfig);

Queue.boot(app.container);
Cache.boot(app.container);
Mail.boot(app.container);

The worker loads bootstrap-cli.ts (or dist/bootstrap-cli.js) instead of bootstrap.ts so it doesn't start the HTTP server.

"Job class '...' is not registered" ​

You dispatched a job but the worker can't find its class. In the latest versions of StruxJS, Job classes inside the app/Jobs directory are discovered automatically. Ensure your job class is exported correctly and resides inside app/Jobs/. If you are dispatching a dynamically created job class, it must be available during startup.

Worker exits immediately ​

If --stop-when-empty is not set, the worker polls forever. If it exits:

  1. Check for uncaught exceptions in your handle() methods
  2. Verify the database/Redis connection is stable
  3. Run with DEBUG=struxjs:* to see detailed logs

Jobs stuck in "reserved" state ​

This happens when a worker crashes mid-job without releasing the reservation. For the database driver, set reservationTimeout in config/queue.ts:

typescript
database: {
    driver: "database",
    reservationTimeout: 90, // seconds before stuck jobs are auto-released
}

After 90 seconds, another worker can pick up the job.


Released under the MIT License.