Reference: Worker Fleet
A reference worker fleet for Node.js 24: BullMQ processors, idempotency, dead-letter monitoring, and pool-aware concurrency - paired with Reference: B2B SaaS API.
Recipe
Quick-reference recipe card.
API publishes job → Redis (BullMQ) → worker pod(s) → Postgres + outbound HTTP
↓ failures
DLQ queue + PagerDuty// Job envelope standard
type JobEnvelope<T> = {
idempotencyKey: string;
tenantId: string;
traceparent?: string;
payload: T;
};Working Example
// src/workers/order-email.worker.ts
import { Worker, Queue } from "bullmq";
import IORedis from "ioredis";
const connection = new IORedis(process.env.REDIS_URL!, { maxRetriesPerRequest: null });
export const orderEmailQueue = new Queue("order-email", { connection });
const worker = new Worker(
"order-email",
async (job) => {
const { idempotencyKey, tenantId, payload } = job.data;
const done = await redis.set(`idem:${idempotencyKey}`, "1", "EX", 86400, "NX");
if (!done) {
job.log("duplicate skipped");
return;
}
await sendEmail(tenantId, payload);
},
{
connection,
concurrency: 5, // pool max 20 on worker; API uses 10
},
);
worker.on("failed", async (job, err) => {
if (job && job.attemptsMade >= 3) {
await dlq.add("failed", { original: job.data, error: String(err) });
}
});// src/main-worker.ts - graceful shutdown
import { orderEmailWorker } from "./workers/order-email.worker";
async function shutdown(signal: string) {
console.info({ msg: "shutdown", signal });
await orderEmailWorker.close();
process.exit(0);
}
process.on("SIGTERM", () => shutdown("SIGTERM"));Monitoring
| Metric | Alert |
|---|---|
bullmq_queue_waiting | > 1000 for 10m |
bullmq_dlq_depth | > 0 for 15m |
job_duration_p95 | > 30s |
pg_pool_waiting | > 0 for 5m on worker |
Deep Dive
Concurrency vs Pool
pool max on worker = 20
concurrency = 5 (not 20 - jobs do 2-4 queries each)
API pool max = 10 (separate Deployment)- Sum of all worker types' peak connections < Postgres
max_connectionsbudget
Idempotency
- Key format:
{tenantId}:{event}:{entityId}e.g.acme:order-email:ord_123 - Store completion in Redis 24h; permanent dedupe in
processed_jobstable for finance
DLQ Playbook
1. Alert fires on DLQ depth
2. On-call inspects first 5 jobs in Bull Board
3. Fix root cause (template bug, vendor 403)
4. Replay DLQ to primary queue in batches of 50
5. Post-mortem if customer emails missed SLAGotchas
- Same image as API - Bloated worker. Fix: Separate Dockerfile target
worker. - No idempotency on webhook send - Duplicate charges. Fix: Keys on every side effect.
- Ignoring SIGTERM - K8s kills mid-job. Fix:
worker.close()on shutdown (see Graceful Shutdown). - Infinite retry on poison - DLQ never fills, queue stuck. Fix: Max attempts 3 then DLQ.
Related
- BullMQ - queue depth
- Idempotency Keys - patterns
- Reference: B2B SaaS API - API pair
- Graceful Shutdown - worker drain
- Case Studies Best Practices - reading references
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.