You're building an AI SaaS. Users submit requests — generate a report, process a document, run an agent task. These take 10-60 seconds. You need a job queue.
Two options dominate: BullMQ (opinionated, high-level) and Redis Streams (primitive, flexible). The wrong choice costs you weeks of rearchitecting.
The Core Difference
BullMQ is a job queue library built on Redis. It handles scheduling, retries, concurrency, priorities, and rate limiting out of the box. You think in jobs and queues.
Redis Streams is a Redis data structure — an append-only log with consumer groups. It's lower-level. You build queue semantics on top of it. You think in messages and consumers.
When BullMQ Wins
BullMQ is the right choice when you need job priorities, cron scheduling, retry logic, or a UI dashboard.
import { Queue, Worker } from 'bullmq';
const queue = new Queue('ai-tasks', { connection: redis });
// High-priority user request
await queue.add('generate', { prompt, userId }, { priority: 1 });
// Cron job
await queue.add('daily-report', {}, {
repeat: { pattern: '0 6 * * *', tz: 'America/Denver' }
});
// Exponential backoff
await queue.add('claude-completion', { prompt }, {
attempts: 5,
backoff: { type: 'exponential', delay: 2000 },
});
Bull Board gives you a free UI dashboard — Redis Streams have no scheduling primitive.
When Redis Streams Win
Streams are better when multiple independent services consume the same event:
# Three consumer groups, one stream
XGROUP CREATE agent-events analytics $ MKSTREAM
XGROUP CREATE agent-events billing $
XGROUP CREATE agent-events audit $
At-least-once processing with acknowledgment:
const messages = await redis.xreadgroup(
'GROUP', 'analytics', 'consumer-1',
'COUNT', 10, 'BLOCK', 5000,
'STREAMS', 'agent-events', '>'
);
// Only acknowledge after confirmed processing
await redis.xack('agent-events', 'analytics', messageId);
// On restart: pending messages replay automatically
Message replay is also native — BullMQ jobs are consumed and removed:
# Replay from beginning for a new consumer
XREAD COUNT 10000 STREAMS agent-events 0-0
The Hybrid Pattern (What Most AI SaaS Actually Uses)
User Request
↓
BullMQ Queue ← scheduling, priority, retry, rate limiting
↓
Worker (Claude call)
↓
Redis Stream ← fan-out to analytics, billing, audit, notifications
const worker = new Worker('ai-tasks', async (job) => {
const result = await runClaudeTask(job.data);
// Publish result to stream for downstream consumers
await redis.xadd('task-completed', '*',
'jobId', job.id,
'userId', job.data.userId,
'tokens', String(result.usage.total_tokens),
'cost', String(estimateCost(result.usage))
);
return result;
}, { connection: redis });
The billing service reads task-completed to charge per-token. Analytics reads the same stream for dashboards. Neither is coupled to BullMQ internals.
Decision Matrix
| Need | BullMQ | Redis Streams |
|---|---|---|
| Job scheduling/cron | yes | no |
| Job priorities | yes | manual |
| Retry with backoff | yes | manual |
| UI dashboard | yes | manual |
| Multi-consumer fan-out | manual | yes |
| Message replay | no | yes |
| High throughput (>10k/s) | limited | yes |
Recommendation
Start with BullMQ pre-PMF. Ergonomics are better, dashboard is free, you don't have time to build retry logic from scratch.
Add Redis Streams when multiple downstream systems (billing, analytics, notifications) need to react to the same events.
Migrate core queue to Streams only if you hit throughput limits. Most AI SaaS products never reach this point.
Building AI SaaS infrastructure from scratch? The AI SaaS Starter Kit ships with BullMQ + Redis Streams wired together — queue processing, event fan-out, and per-user rate limiting included.
Top comments (0)