Background Jobs in SaaS: Inngest vs BullMQ vs Trigger.dev in 2026
TL;DR
Inngest for Vercel-hosted Next.js apps — serverless-native, no Redis required, great DX. BullMQ for apps on Railway/Render with a persistent Redis instance — battle-tested, familiar API. Trigger.dev for complex job orchestration, fan-out, and team-facing job visibility. Most indie SaaS starts with Inngest.
Why Background Jobs Matter
Every SaaS hits the same pattern: an action takes too long for a synchronous HTTP response.
User signs up → Send welcome email ← 200-500ms, okay
User upgrades → Process invoice + notify team ← 500ms+, getting slow
User exports data → Generate PDF/CSV ← 5-60s, must be async
User uploads video → Transcode to multiple formats ← Minutes, definitely async
Background jobs move long-running work out of the request/response cycle.
Inngest: Serverless-Native Jobs
Inngest is designed for serverless environments. No Redis, no persistent server — jobs are triggered via HTTP and executed as serverless functions.
// lib/inngest.ts
import { Inngest } from 'inngest';
export const inngest = new Inngest({ id: 'my-saas' });
// Define a function
export const sendWelcomeEmail = inngest.createFunction(
{ id: 'send-welcome-email' },
{ event: 'user/signed.up' },
async ({ event, step }) => {
// step.run() — each step is retried independently on failure
const user = await step.run('get-user', async () => {
return prisma.user.findUnique({ where: { id: event.data.userId } });
});
await step.run('send-email', async () => {
await resend.emails.send({
to: user!.email,
subject: 'Welcome!',
react: <WelcomeEmail name={user!.name} />,
});
});
// Sleep for 3 days, then check if user onboarded
await step.sleep('wait-for-onboarding', '3 days');
const updatedUser = await step.run('check-onboarding', async () => {
return prisma.user.findUnique({ where: { id: event.data.userId } });
});
if (!updatedUser?.onboardedAt) {
await step.run('send-onboarding-nudge', async () => {
await resend.emails.send({
to: updatedUser!.email,
subject: 'Haven't finished setup yet?',
react: <OnboardingNudgeEmail />,
});
});
}
}
);
// app/api/inngest/route.ts — Inngest handler
import { serve } from 'inngest/next';
import { inngest, sendWelcomeEmail } from '@/lib/inngest';
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [sendWelcomeEmail],
});
// Trigger from API route — fire and forget
await inngest.send({ name: 'user/signed.up', data: { userId: user.id } });
Inngest Strengths
- No infrastructure: No Redis, no persistent worker — Inngest Cloud handles it
- Step-level retries: Each
step.run()retries independently. If step 3 fails, steps 1-2 don't re-run. - Sleep / scheduling:
step.sleep()andstep.sleepUntil()for delayed actions - Observable: Inngest dashboard shows every job run, step, and failure
- Vercel-native: Scales with your serverless functions
Inngest Limitations
- Cold starts: Serverless functions have cold start latency
- Rate limits on free tier (50k function runs/month)
- Less control over concurrency than BullMQ
- Requires Inngest Cloud (or self-hosted) for job tracking
BullMQ: Battle-Tested Redis Queues
BullMQ is the most mature Node.js job queue. Uses Redis as a broker, runs as persistent workers.
// lib/queues.ts — queue definitions
import { Queue, Worker, QueueEvents } from 'bullmq';
import { Redis } from 'ioredis';
const connection = new Redis(process.env.REDIS_URL!, {
maxRetriesPerRequest: null,
});
export const emailQueue = new Queue('emails', { connection });
export const pdfQueue = new Queue('pdf-generation', { connection });
// Worker — runs as a separate process on Railway/Render
const emailWorker = new Worker(
'emails',
async (job) => {
switch (job.name) {
case 'welcome':
await sendWelcomeEmail(job.data.userId);
break;
case 'password-reset':
await sendPasswordResetEmail(job.data.userId, job.data.token);
break;
default:
throw new Error(`Unknown job type: ${job.name}`);
}
},
{
connection,
concurrency: 20, // Process 20 jobs simultaneously
limiter: {
max: 100,
duration: 1000, // Max 100 jobs per second (rate limit)
},
}
);
// Priority queues
await emailQueue.add('welcome', { userId }, {
priority: 1, // Lower = higher priority
attempts: 3,
backoff: { type: 'exponential', delay: 2000 },
});
// Scheduled/cron jobs
await emailQueue.add(
'weekly-digest',
{},
{ repeat: { cron: '0 9 * * 1' } } // Every Monday 9am
);
BullMQ Strengths
- Mature, battle-tested (10+ years of history)
- Powerful job prioritization, rate limiting, concurrency control
- Cron scheduling built-in
- Excellent for high-volume job processing
- Bull Board UI for monitoring
BullMQ Limitations
- Requires Redis (Upstash doesn't support BullMQ — need Railway/Render Redis)
- Requires persistent worker process (not serverless)
- More infrastructure to manage
Trigger.dev: The Orchestration Platform
Trigger.dev focuses on complex, observable job workflows:
// Trigger.dev v3
import { task, schedules } from '@trigger.dev/sdk/v3';
export const onboardingSequence = task({
id: 'onboarding-sequence',
run: async (payload: { userId: string }) => {
// Fan-out to parallel tasks
const [user, subscription] = await Promise.all([
getUser(payload.userId),
getSubscription(payload.userId),
]);
// Conditional branching
if (subscription?.plan === 'pro') {
await sendProWelcomeEmail(user);
await scheduleProOnboardingCall(user);
} else {
await sendFreeWelcomeEmail(user);
}
// Wait and check
await new Promise(resolve => setTimeout(resolve, 3 * 24 * 60 * 60 * 1000));
const hasOnboarded = await checkOnboardingComplete(payload.userId);
if (!hasOnboarded) {
await sendOnboardingNudge(user);
}
},
});
Choose Trigger.dev when:
- Complex multi-step workflows with branching
- Need team-visible job runs (customer success can see what happened)
- AI agent tasks that need to run for minutes/hours
- Fine-grained observability is a product requirement
When to Add Each
| Signal | Add |
|---|---|
| Email sending is slow | Inngest or BullMQ |
| File processing (PDF, video, images) | Inngest or BullMQ |
| Webhook processing with retries | Inngest |
| Scheduled cron jobs | Inngest, BullMQ, or Vercel Cron |
| High-volume job processing (>100k/day) | BullMQ |
| Complex orchestration with branching | Trigger.dev |
| Multi-step with independent retries | Inngest |
Boilerplate Inclusion
| Boilerplate | Background Jobs | Provider |
|---|---|---|
| ShipFast | ❌ | — |
| Supastarter | ❌ | — |
| Makerkit | ✅ | Inngest |
| T3 Stack | ❌ | Add yourself |
| Open SaaS | ✅ | Inngest |
Find boilerplates with background job setup on StarterPick.
Check out this boilerplate
View Inngest on StarterPick →