RoyceTL;DR Inngest for Vercel-hosted Next.js apps — serverless-native, no Redis required, great...
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.
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 is designed for serverless environments. No Redis, no persistent server — jobs are triggered via HTTP and executed as serverless functions.
// lib/inngest.ts
// Define a function
{ 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
client: inngest,
functions: [sendWelcomeEmail],
});
// Trigger from API route — fire and forget
await inngest.send({ name: 'user/signed.up', data: { userId: user.id } });
step.run() retries independently. If step 3 fails, steps 1-2 don't re-run.step.sleep() and step.sleepUntil() for delayed actionsBullMQ is the most mature Node.js job queue. Uses Redis as a broker, runs as persistent workers.
// lib/queues.ts — queue definitions
const connection = new Redis(process.env.REDIS_URL!, {
maxRetriesPerRequest: null,
});
// 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
);
Trigger.dev focuses on complex, observable job workflows:
// Trigger.dev v3
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:
| 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 | Background Jobs | Provider |
|---|---|---|
| ShipFast | ❌ | — |
| Supastarter | ❌ | — |
| Makerkit | ✅ | Inngest |
| T3 Stack | ❌ | Add yourself |
| Open SaaS | ✅ | Inngest |
Find boilerplates with background job setup on StarterPick.