Queue Worker Skill
BullMQ worker with retries and DLQ checklist - an Agent Skill for async job processing on Node.js 24 with Redis.
What This Skill Does
Produces a worker bootstrap plan: queue definition, worker process entry, payload schema, retry/DLQ policy, observability hooks, and deployment split from the HTTP API.
When to Invoke
- Adding email, webhook, or export jobs to an existing API
- Moving synchronous work out of request path (502/timeout relief)
- Standardizing retry behavior across services
- Creating a new worker-only deployable in the monorepo
Inputs
| Input | Why |
|---|---|
| Queue name | email, webhooks-outbound |
| Job payload shape | Zod schema fields |
| Concurrency target | Worker pod CPU and upstream rate limits |
| Redis URL | Shared cluster or dedicated queue Redis |
| Idempotency needs | Payment-like jobs need dedup keys |
Outputs
src/queues/<name>.queue.ts- Queue producersrc/workers/<name>.worker.ts- Worker consumersrc/schemas/<name>.job.ts- Zod payload schemapackage.jsonscript:"worker:<name>": "node dist/workers/<name>.worker.js"- DLQ queue or failed job inspection steps
- Verification commands
Guardrails
- Workers run in separate process - never
new Worker()inside HTTPserver.ts. maxRetriesPerRequest: nullon ioredis connections used by BullMQ workers.- Validate payload with Zod -
parsethrows → BullMQ retry; log poison pills. - No unbounded
removeOnComplete: true- keep last N for debugging. - Retry POST-like side effects only with idempotency keys stored in Postgres.
- SIGTERM:
await worker.close()before exit for in-flight jobs. - Log
jobId,attemptsMade, not full PII payload.
Recipe
Quick-reference recipe card - copy-paste ready.
# Verification after skill output
export REDIS_URL=redis://localhost:6379
npm run build
npm run worker:email &
WORKER_PID=$!
node -e "
import { emailQueue } from './dist/queues/email.queue.js';
const job = await emailQueue.add('send', { to: 'test@example.com', templateId: 'welcome' });
console.log('enqueued', job.id);
"
sleep 5
redis-cli LLEN bull:email:failed # expect 0 on success
kill -TERM $WORKER_PID// src/schemas/email.job.ts
import { z } from "zod";
export const emailJobSchema = z.object({
to: z.string().email(),
templateId: z.string(),
vars: z.record(z.string()).default({}),
});
export type EmailJob = z.infer<typeof emailJobSchema>;// src/workers/email.worker.ts
import { Worker } from "bullmq";
import { createRedis } from "../redis.js";
import { emailJobSchema } from "../schemas/email.job.js";
import { sendEmail } from "../services/email.js";
const worker = new Worker(
"email",
async (job) => {
const payload = emailJobSchema.parse(job.data);
await sendEmail(payload);
},
{
connection: createRedis(),
concurrency: 5,
}
);
async function shutdown() {
await worker.close();
process.exit(0);
}
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);DLQ Checklist
-
attemptsset per job type (3-5 typical) -
backoff: { type: "exponential", delay: 2000 } - Failed jobs after max attempts copied to
email-dlqqueue OR retained infailedset - Runbook: how to replay DLQ job manually
- Alert on
failedcount threshold in Redis/Bull Board - Dashboard: queue depth, processing rate, stalled count
Example Prompts
Use Queue Worker Skill:
- Queue: webhooks-outbound
- Payload: { url, secret, eventType, body }
- Concurrency: 20
- Idempotency: eventId unique in Postgres
- Service: notifications-api (Fastify 5)FAQs
Same repo as API or separate worker package?
Same repo is fine with separate npm run worker:* and K8s Deployment. Split packages when worker deps (PDF libs) bloat API image size.
How long should job handlers run?
Target under 30s. Longer work should checkpoint or split into chained jobs. BullMQ stalled job detection punishes blocking CPU.
Related
- bullmq / ioredis - pairing guide
- BullMQ - queue depth patterns
- zod - payload validation
- Agent Skills Basics - invocation rules
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.