Skip to content
Back to skills

Durable Task Queues

ASecurity

Durable background job and task queue patterns for AI agent systems. BullMQ Redis-backed queues with concurrency + rate limiting, Inngest event-driven durable functions, Trigger.dev background tasks, dead-letter queue patterns, and job failure diagnostics. Sources: taskforcesh/bullmq, inngest/inngest, triggerdotdev/trigger.dev, OptimalBits/bull, agenda/agenda.

  • 10 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 9, 2026
ai-agentstypescriptgonextjsapidatabase

Works with

  • api

Security analysis

A100/100

Scanned September 9, 2026

npx -y skills add yanacuti1121/Yana-AI --skill durable-task-queues --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Durable Task Queues?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Durable Task Queues
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/yanacuti1121-durable-task-queues/badge)](https://www.skillsdirectory.com/skills/yanacuti1121-durable-task-queues)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: durable-task-queues
description: Durable background job and task queue patterns for AI agent systems. BullMQ Redis-backed queues with concurrency + rate limiting, Inngest event-driven durable functions, Trigger.dev background tasks, dead-letter queue patterns, and job failure diagnostics. Sources: taskforcesh/bullmq, inngest/inngest, triggerdotdev/trigger.dev, OptimalBits/bull, agenda/agenda.
origin: yana-ai — synthesized from taskforcesh/bullmq, inngest/inngest, triggerdotdev/trigger.dev, OptimalBits/bull, agenda/agenda
license: Apache-2.0
version: 1.0.0
compatibility: yana-ai >= 1.3.44
---

# /durable-task-queues

## When to Use

- Agent task takes > 30s — HTTP response timeout risk
- Work must survive server restart (embedding batch, export, analysis)
- Fan-out: one event → many parallel sub-tasks
- "Why did this job fail last night?" — need structured job history

## Do NOT use for

- Sub-second operations — queue overhead isn't worth it
- One-off scripts run manually

---

## Decision: BullMQ vs Inngest vs Trigger.dev

```
Already using Redis in infra?
  YES → BullMQ (battle-tested, mature, full queue primitives)
  NO  →
    Want event-driven, serverless-compatible, no extra infra?
      YES → Inngest (zero infra, SDK only, Vercel/Next.js native)
      Need long-running background tasks (> 15 min) with retries + logging?
        YES → Trigger.dev (dashboard, streaming logs, sleep/wait APIs)
```

---

## BullMQ (Redis-backed queues)

```typescript
import { Queue, Worker, QueueEvents } from 'bullmq'

const connection = { host: process.env.REDIS_HOST, port: 6379 }

// Queue — producer side
const agentQueue = new Queue('agent-tasks', {
  connection,
  defaultJobOptions: {
    attempts:    3,
    backoff:     { type: 'exponential', delay: 2000 },  // 2s, 4s, 8s
    removeOnComplete: { count: 100 },   // keep last 100 completed
    removeOnFail:     { count: 500 },   // keep last 500 failed for debug
  },
})

await agentQueue.add('run-analysis', { repoUrl, sessionId }, {
  priority: 1,             // lower = higher priority (1 = urgent)
  delay:    0,
  jobId:    `analysis:${sessionId}`,  // idempotent ID prevents duplicates
})

// Worker — consumer side
const worker = new Worker('agent-tasks', async (job) => {
  job.log(`Starting analysis for ${job.data.repoUrl}`)
  await job.updateProgress(10)

  const result = await runAgentAnalysis(job.data.repoUrl)
  await job.updateProgress(100)
  return result

}, {
  connection,
  concurrency:    5,      // max parallel jobs per worker
  limiter: {
    max:          10,     // rate limit: max 10 jobs
    duration:     1000,  // per 1000ms
  },
})

worker.on('failed', (job, err) => {
  logger.error({ jobId: job?.id, err: err.message }, 'job_failed')
})

// Rule: always set jobId for idempotency — re-enqueuing same ID is a no-op
// Rule: removeOnComplete + removeOnFail required — Redis fills up without them
// Rule: job.log() goes to BullMQ job history, visible in BullBoard
```

```typescript
// Dead-letter queue pattern — jobs that exhausted all retries
const deadLetterQueue = new Queue('agent-tasks-dlq', { connection })

worker.on('failed', async (job, err) => {
  if (job && job.attemptsMade >= (job.opts.attempts ?? 1)) {
    await deadLetterQueue.add('dead', {
      originalJob:  job.name,
      data:         job.data,
      error:        err.message,
      failedAt:     new Date().toISOString(),
    })
  }
})

// Rule: DLQ enables manual replay — never silently drop failed jobs
```

---

## Inngest (event-driven durable functions)

```typescript
import { Inngest } from 'inngest'

const inngest = new Inngest({ id: 'yamtam-agent' })

// Define a durable function — survives cold starts, serverless-compatible
export const processAgentTask = inngest.createFunction(
  {
    id:     'process-agent-task',
    name:   'Process Agent Task',
    retries: 3,
    throttle: { limit: 10, period: '1m', key: 'event.data.userId' },
  },
  { event: 'agent/task.created' },

  async ({ event, step }) => {
    // Each step is independently retried — completed steps are skipped on retry
    const embeddings = await step.run('compute-embeddings', async () => {
      return computeEmbeddings(event.data.content)
    })

    const stored = await step.run('store-to-vector-db', async () => {
      return storeEmbeddings(embeddings, event.data.sessionId)
    })

    // Sleep without blocking a thread (Inngest persists state)
    await step.sleep('wait-for-cooldown', '5s')

    const result = await step.run('run-analysis', async () => {
      return runAgentAnalysis(event.data.taskId)
    })

    return { stored, result }
  }
)
```

```typescript
// Send event to trigger the function
await inngest.send({
  name: 'agent/task.created',
  data: { taskId: uuid(), userId, content, sessionId },
})

// Rule: step.run() = durable checkpoint — data is persisted between steps
// Rule: step.sleep() does not consume a thread — safe for minutes/hours
// Rule: throttle.key scopes rate limit per field (e.g., per userId)
// Rule: Inngest replays from last successful step on retry — not from start
```

---

## Trigger.dev (long-running background tasks)

```typescript
import { task, schedules, wait } from '@trigger.dev/sdk/v3'

// Long-running task with streaming logs (visible in Trigger.dev dashboard)
export const agentResearchTask = task({
  id:      'agent-research',
  retry:   { maxAttempts: 3, minTimeoutInMs: 1000, multiplier: 2 },
  machine: { preset: 'medium-1x' },  // 1 vCPU / 2GB RAM

  run: async (payload: { topic: string; sessionId: string }) => {
    console.log(`[research] Starting: ${payload.topic}`)

    const sources = await searchWeb(payload.topic)
    console.log(`[research] Found ${sources.length} sources`)

    // wait.for suspends task (not sleeping thread) — supports hours
    await wait.for({ seconds: 5 })

    const summaries = await Promise.all(
      sources.map(src => summarizeSource(src))
    )

    const report = await synthesizeReport(summaries, payload.topic)
    console.log(`[research] Done — ${report.length} chars`)
    return { report, sourceCount: sources.length }
  },
})

// Trigger from API route
import { tasks } from '@trigger.dev/sdk/v3'

const handle = await tasks.trigger('agent-research', {
  topic:     'vector database benchmarks 2025',
  sessionId: req.session.id,
})

// Poll or webhook for completion
const result = await tasks.poll(handle.id, { intervalMs: 2000 })
```

---

## Job Failure Diagnostics Pattern

```typescript
// Structured failure logging for post-mortem
worker.on('failed', async (job, err) => {
  if (!job) return

  const diagnostic = {
    jobId:      job.id,
    jobName:    job.name,
    attempt:    job.attemptsMade,
    maxAttempts: job.opts.attempts,
    data:       job.data,             // scrub PII before logging
    error:      err.message,
    stack:      err.stack,
    startedAt:  new Date(job.processedOn!).toISOString(),
    failedAt:   new Date().toISOString(),
    logs:       await job.logs,       // job.log() entries
  }

  logger.error(diagnostic, 'job_failure_diagnostic')
  // Alert if final attempt
  if (job.attemptsMade >= (job.opts.attempts ?? 1)) {
    await alertSlack(`Job ${job.name} exhausted retries: ${err.message}`)
  }
})

// Rule: log job.data in failure diagnostic — can't reproduce without input
// Rule: alert only on final attempt — intermediate retries are expected noise
```

---

## Anti-Fake-Pass Checklist

```
❌ BullMQ worker without concurrency limit (Redis connection pool exhausted)
❌ No removeOnComplete/removeOnFail (Redis OOM in hours)
❌ Retry without backoff on rate-limited API (thundering herd on 429)
❌ jobId missing (duplicate jobs on re-enqueue = duplicate side effects)
❌ Inngest step.run() not used (whole function re-runs on any step failure)
❌ DLQ absent (failed jobs silently lost — can't diagnose or replay)
❌ Trigger.dev task without machine preset (memory default too low for LLM ops)
❌ Job failure handler logs only error message (missing input data = can't reproduce)
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.