Some operations are too slow to run while a user waits — sending 10,000 emails, processing a video, or calling an AI model multiple times. A message queue lets your app hand off that work to a background process, respond immediately to the user, and complete the work reliably in the background.
If Cartara flagged this in your diff, you likely added a job queue, a background worker, or code that enqueues tasks rather than handling them inline.
The Problem Queues Solve
Without a queue, slow work blocks the user:
```
User clicks "Generate report"
→ App calls AI model (15 seconds)
→ App processes results (5 seconds)
→ App sends email (2 seconds)
→ User finally sees "done" (22 seconds later, if the request didn't time out)
```
With a queue:
```
User clicks "Generate report"
→ App puts job on queue (instant)
→ App responds: "Report is generating — we'll email you when it's ready"
→ Worker picks up job in the background
→ Worker calls AI, processes results, sends email
→ User gets an email when it's done
```
The user experience is dramatically better. The work happens reliably even if it takes minutes.
The Core Pattern
```
User action
↓
App enqueues a job → returns immediately (fast response to user)
↓
Worker picks up job
↓
Does the work (email, AI call, etc.)
↓
Marks job complete (or retries on failure)
```
Queues vs. Pub/Sub
Message Queue (point-to-point): A job goes to exactly one worker. Used for background task processing — sending emails, generating reports, resizing images.
Pub/Sub (publish-subscribe): A message goes to all subscribers of a topic. Used for notifying multiple services about an event — "order placed" might trigger payment processing, inventory updates, and an email, all independently.
For most early-stage apps, a simple job queue is what you need.
What You'll See in Your Code
Queue-related code typically appears in diffs as:
Enqueuing a job instead of doing work inline:
```ts
// ❌ Old: blocks the request handler
app.post('/send-welcome', async (req, res) => {
await sendWelcomeEmail(req.body.userId); // slow
res.json({ success: true });
});
// ✅ New: enqueue and respond immediately
app.post('/send-welcome', async (req, res) => {
await queue.add('send-welcome-email', { userId: req.body.userId });
res.json({ success: true }); // returns instantly
});
```
A worker file that processes jobs:
```ts
// worker.ts
queue.process('send-welcome-email', async (job) => {
const { userId } = job.data;
const user = await db.users.findById(userId);
await emailService.sendWelcome(user.email);
});
```
Queue configuration files for tools like BullMQ, Inngest, or Trigger.dev.
Key Concepts
Idempotency
Workers must be able to process the same job twice without causing duplicate side effects. Networks fail; messages can be delivered more than once. If your email job fires twice, one email should send — not two.
```ts
// Check if we've already processed this job
const alreadySent = await db.emailLog.findOne({ jobId: job.id });
if (alreadySent) return; // skip duplicate
await emailService.send(user.email);
await db.emailLog.create({ jobId: job.id });
```
Dead Letter Queues (DLQ)
When a job fails repeatedly (after several retries), it moves to a Dead Letter Queue — a separate queue for permanently failed jobs. This prevents a broken job from blocking the rest of the queue while making sure you don't silently lose it.
Monitor your DLQ. Entries there represent real failures in your system.
Retries with Backoff
Good queue systems automatically retry failed jobs with increasing delays — retry after 1 second, then 5 seconds, then 30 seconds. This handles transient failures (network blips, temporary service unavailability) without losing jobs.
Visibility Timeout
When a worker picks up a job, it becomes invisible to other workers for a set period. If the worker completes and acknowledges the job, it's removed. If the worker crashes without acknowledging, the job becomes visible again for another worker. Set this longer than your expected job duration.
Queue Tools
| Tool | Best For |
|---|---|
| Inngest | Serverless-friendly, great DX, handles retries and scheduling well |
| Trigger.dev | Complex multi-step background jobs with a visual dashboard |
| BullMQ | Redis-based, widely used in Node.js, good for self-hosted setups |
| AWS SQS | Battle-tested, scalable, tight AWS ecosystem integration |
| Upstash QStash | Serverless-native, HTTP-based, pay-per-request |
For most small apps and serverless environments, Inngest or Trigger.dev are the easiest starting points.
Async Processing for AI Workloads
LLM calls are slow (2–30 seconds) and expensive — ideal candidates for queues:
- User submits a task (document analysis, report generation)
- App enqueues the job and returns a job ID immediately
- Worker processes the job asynchronously, calling the LLM and storing results
- User polls for status or receives a notification when done
For batch tasks involving many LLM calls (process 500 documents), queue each document as a separate job so workers can process them in parallel. What would take 90 minutes serially can take 3 minutes with parallel workers.