Workers as separate processes

The API server and the worker should run as separate processes. If the worker crashes on a bad PDF, the API keeps serving requests. You can also scale them independently: one API server and five workers during peak hours. Same code base, separate lifecycles.

after/worker.ts
typescript
import { Worker, Job } from 'bullmq';
import { getRedisConnectionOpts } from './queue';
import { db } from './db';
import { orders } from './db/schema';
import { eq } from 'drizzle-orm';

const worker = new Worker(
  'OrderConfirmations',
  async (job: Job) => {
    const { orderId } = job.data;

    // Simulated heavy work: PDF generation, email delivery, etc.
    await new Promise((resolve) => setTimeout(resolve, 3000));

    await db.update(orders).set({ status: 'completed' }).where(eq(orders.id, orderId));
  },
  { connection: getRedisConnectionOpts() },
);

worker.on('failed', (job, err) => {
  console.error('Job ' + job?.id + ' failed: ' + err.message);
});

process.on('SIGINT', async () => {
  await worker.close();
  process.exit(0);
});

A standalone Node process that watches the OrderConfirmations queue.

The Worker constructor takes the queue name, a handler function, and connection options. The handler runs once per job. Inside it you do whatever the job needs: call external services, touch the database, generate files. On failure, BullMQ retries the job automatically based on its config. On SIGINT, the close call lets the current job finish before the process exits. That is a graceful shutdown in five lines.

In local development, you run the API and the worker in two terminals. npm run start:api in one, npm run start:worker in the other. In production, they are usually two separate Docker containers or two separate Kubernetes deployments. Either way, same queue, different processes.

Quiz: Quiz

Loading practice…