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
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:
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
},
})| Attempt | Delay Before Next Retry |
|---|---|
| 1st failure | ~10 seconds |
| 2nd failure | ~20 seconds |
| Each next failure | Doubles, capped at 1 hour |
| Final failure | Marked 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.
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