Documentation

Advanced

Queue Tasks

Run asynchronous background tasks through VitNode's database-backed queue with retries and AdminCP monitoring.

Queue tasks handle asynchronous, one-off background work (e.g. sending bulk emails, processing media, or scheduling posts). Tasks are persisted in the core_queue table and drained periodically by VitNode's background worker.

Quick start

1. Define a Queue Task

plugins/blog/src/api/tasks/send-newsletter.task.ts
import { buildQueueTask } from "@vitnode/core/api/lib/queue"

export interface NewsletterPayload {
  postId: number
}

export const sendNewsletterTask = buildQueueTask<NewsletterPayload>({
  name: "send-newsletter",
  handler: async (c, payload) => {
    await c.get("log").info(`Processing newsletter for post #${payload.postId}`)
  },
})

2. Register in an API Module

Attach the task to your module's queueTasks list:

plugins/blog/src/api/modules/posts/posts.module.ts
import { buildModule } from "@/api/builders"
import { sendNewsletterTask } from "../../tasks/send-newsletter.task"

export const postsModule = buildModule({
  name: "posts",
  routes: [createPostRoute],
  queueTasks: [sendNewsletterTask], 
})

3. Dispatch Tasks

Dispatch jobs from any Hono route or service:

// Dispatch immediately
await c.get("queue").dispatch({
  name: "send-newsletter",
  payload: { postId: 42 },
})

// Or schedule for future execution
await c.get("queue").dispatch({
  name: "send-newsletter",
  payload: { postId: 42 },
  executeAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour from now
  priority: 10, // Higher numbers run first
})

dispatch returns { id } as soon as the task row is written to PostgreSQL.


Retries and Error Handling

VitNode tasks automatically retry on failure using exponential backoff:

export const riskyTask = buildQueueTask({
  name: "sync-external-api",
  maxAttempts: 5, // Default is 3
  handler: async (c, payload) => {
    // If an error is thrown, the task retries automatically
  },
})
AttemptDelay Before Next Retry
1st failure~10 seconds
2nd failure~20 seconds
Each next failureDoubles, capped at 1 hour
Final failureMarked as failed in core_queue

How the Worker Drains the Queue

Each process-queue tick claims 25 tasks at a time and runs up to 5 of them at once, then claims the next 25 - until the queue is empty or about 45 seconds have passed. Handlers share one tick, so make them safe to run side by side. Finished tasks stay in core_queue for 7 days; the hourly clean job removes older ones in small batches.

Stale Tasks Are Reclaimed

A task still processing after 15 minutes (QUEUE_STALE_PROCESSING_MINUTES) belonged to a worker that died mid-run. The queue worker puts it back to pending, or marks it failed if it has used every attempt, so a crash never strands a task forever.

Keep handlers well under 15 minutes

A handler that runs longer would be reclaimed and run again while it is still going. Long work should save its progress and dispatch a follow-up task instead - the way notification fan-out does.


Inspect tasks in the Debug Panel

Open Debug Panel → Queue in the AdminCP (/admin/core/debug/queue), or type "Queue tasks" into the ⌘K admin search. The table lists every task with its queue, status, attempts, when it is next available, when it was created and its last error. Filter by status to find the failed ones.

The section is read-only. A failed task stays failed, so fix the cause and dispatch it again from your code. Viewing it needs the queue → can_view staff permission.


buildQueueTask Options

Prop

Type

Learn More