JCC Express

Digging Deeper

Queues

Introduction

Queues defer work (emails, exports, webhooks) to background workers. JCC Express supports memory, database, and Redis-style drivers (per app/Config/.ts). Dispatch with app.resolve("Queue") or the global dispatch(...) helper, then run bun artisanNode :work. A web dashboard under / is registered by default (see Queue dashboard).

Configuration

The configuration file is stored in app/Config/.ts.

TypeScript
export default {
  default: process.env.QUEUE_CONNECTION || "memory",

  connections: {
    memory: {
      driver: "memory",
      : "default",
    },

    database: {
      driver: "database",
      table: "jobs",
      : "default",
      retry_after: 90,
    },
  },
};

Driver Prerequisites

Database

In order to use the database driver, you will need a database table to hold the jobs. To generate a migration that creates this table, run the make:-table Artisan command:

Bash
bun artisanNode make:-table
bun artisanNode migrate

Queue dashboard (built-in web UI)

The framework ships with a small queue management UI, registered automatically when the queue service provider boots. Routes are mounted under the /queue prefix.

Method & pathPurpose
GET /queue/dashboardDashboard (jsBlade view)
GET /queue/jobsJSON list of queues (intended for API consumers)
GET /queue/jobs/{status}Jobs for a given status (e.g. pending, failed)
PUT / PATCH /queue/jobs/{id}/{status}Retry a failed job or run a pending / processing job from the UI
DELETE /queue/jobs/{id}Remove a job from the store
POST /queue/jobsHook for running jobs (body handled by controller)

Views: the starter app includes resources/views/queue/dashboard.blade.html and resources/views/queue/jobs.blade.html. Copy or customize those under your app’s resources/views/queue/ as needed.

Guards: Queue dashboard routes use queueDashboardGuard. When APP_ENV is local, the guard does nothing (open access for development). Otherwise it runs JWT cookie auth, then checks the viewJobs gate ability. Define viewJobs in a service provider (e.g. Gate.define("viewJobs", (user) => …)). Unauthenticated users are redirected to /login; authenticated users without the ability get 403. Import queueDashboardGuard from jcc-express-mvc/Queue if you need to reuse it on custom routes.

Creating Jobs

Generating Job Classes

To generate a new job class, use the make:job Artisan command or create a class extending Job.

TypeScript
import { Job } from "jcc-express-mvc/Queue";

export class ProcessPodcast extends Job {
  // The number of times the job may be attempted.
  public maxAttempts = 5;

  // The number of seconds the job can run before timing out.
  public timeout = 120;

  constructor(public podcast: any) {
    super(podcast);
  }

  /**
   * Execute the job.
   */
  async handle() {
    // Process the podcast...
  }

  /**
   * Calculate the number of seconds to wait before retrying the job.
   */
  public backoff(attempt: number): number {
    return Math.pow(2, attempt) * 1000; // Exponential backoff in ms
  }
}

Dispatching Jobs

Once you have written your job class, you may dispatch it using the service or the global dispatch(...) helper.

TypeScript
import { app } from "@/bootstrap/app";
import { ProcessPodcast } from "@/Jobs/ProcessPodcast";

const  = app.resolve("Queue");

// Dispatch immediately
await .push(new ProcessPodcast(podcast));

// Dispatch with delay
await .later(new ProcessPodcast(podcast), 10); // 10 seconds delay

Using the global helper:

TypeScript
dispatch(new ProcessPodcast(podcast));

Running The Queue Worker

JCC Express includes a worker that will process new jobs as they are pushed onto the queue. You may run the worker using the queue:work Artisan command:

Bash
# Run the worker for the default queue
bun artisanNode queue:work

# Run the worker for a specific queue
bun artisanNode queue:work emails

Worker Concurrency

By default the worker processes one job at a time. You can run up to 5 concurrent workers by passing the concurrency option:

Bash
# 3 concurrent workers on the default queue
bun artisanNode queue:work default concurrency=3

# Maximum concurrency (capped at 5)
bun artisanNode queue:work default concurrency=5

Each worker runs its own independent polling loop. When a queue is empty every worker sleeps for one second before checking again, so idle workers have no meaningful overhead.

OptionDefaultMaxDescription
concurrency15Number of jobs processed in parallel

Database driver note: concurrent workers require MySQL 8+ or PostgreSQL 9.5+. The database driver uses SELECT … FOR UPDATE SKIP LOCKED inside a transaction so each worker atomically claims a unique job — no two workers will ever process the same job at the same time. SQLite does not support this locking syntax and should not be used with concurrency > 1.

Handling Failed Jobs

Retries & Backoff

If an exception is thrown while the job is being processed, the job will automatically be released back onto the to be retried. The job will be retried until it has reached the maximum number of attempts defined by your maxAttempts property.

By default, jobs use an exponential backoff. You may customize this by overriding the backoff method in your job class.

The Failed Jobs Table

When a job exceeds its maximum attempts, it will be moved into the failed_jobs database table. This table contains the job's payload, exception information, and the time it failed.

To create the failed_jobs table, ensure you have run the table migration:

Bash
bun artisanNode make:-table
bun artisanNode migrate

Viewing Failed Jobs

Use the ** dashboard** (//jobs/failed) when the database driver is in use, or inspect the failed_jobs table directly:

TypeScript
const failed = await DB.table("failed_jobs").get();