Message queues let one part of a system publish work for another part to process later. They are useful when work is slow, unreliable, or does not need to block the user’s request.
Producers and Consumers
type EmailJob = { kind: "send_welcome_email"; userId: string;};async function signup(email: string) { const user = await users.create({ email }); await queue.publish<EmailJob>({ kind: "send_welcome_email", userId: user.id }); return user;}
The signup request can return quickly. A worker process consumes the job and sends the email separately.
Idempotent Jobs
Queues usually provide at-least-once delivery, which means a job may run more than once. Job handlers must tolerate duplicates.
async function sendWelcomeEmail(job: EmailJob) { const alreadySent = await emails.wasSent(job.userId, "welcome"); if (alreadySent) return; await emailProvider.sendWelcome(job.userId); await emails.markSent(job.userId, "welcome");}
WARNING
Retrying a non-idempotent job can double-charge a card, send duplicate notifications, or create repeated records.
When to Use a Queue
Use queues for emails, image processing, imports, reports, webhooks, and third-party API calls. Avoid queues when the caller needs the result immediately or when the added operational complexity is not justified.
TIP
Start with one queue and a small worker. Add routing, priorities, and dead-letter queues once real operational needs appear.
Further Learning
Search these terms to go deeper:
“message queue producer consumer pattern” — the core model
“idempotent background jobs” — safe retries and duplicate handling
“dead letter queue explained” — managing permanently failing jobs
“RabbitMQ vs SQS vs Redis queue” — common queue choices
Message Queues
Some tasks are slow — sending an email, resizing an image, generating a report. If you make the user wait for these during their request, the app feels sluggish. A message queue lets you say “do this later” and immediately respond to the user.
The idea
Instead of doing the slow work right away, drop a note in a queue describing what needs to happen. A separate worker picks it up and does it whenever it can:
async function signup(email) { const user = await createUser(email) await queue.publish({ kind: 'send_welcome_email', userId: user.id }) return user // returns immediately — email sends in the background}
The signup request finishes fast; the email goes out a moment later.
The must-know rule: expect duplicates
Queues sometimes deliver the same job twice (better safe than lost). So your job code must handle running twice safely — this is called being idempotent:
async function sendWelcomeEmail(job) { const alreadySent = await wasSent(job.userId, 'welcome') if (alreadySent) return // skip if we already did this await sendEmail(job.userId) await markSent(job.userId, 'welcome')}
WARNING
If a job isn’t designed to handle running twice, a retry could double-charge a card or send a duplicate email. Always check “have I already done this?” before acting.
When to use one
Great for: emails, image processing, reports, third-party API calls — anything slow that doesn’t need to finish before you respond to the user. Skip it when the caller genuinely needs the result immediately.
In one sentence
A message queue lets slow work happen in the background instead of blocking the user’s request — just make sure your background jobs handle running twice safely (idempotent), since queues can occasionally redeliver a job.
Want to go deeper?
Switch to Expert mode above for retry backoff strategies and dead-letter queues.