Skip to main content

Guide

Inngest vs BullMQ vs Trigger.dev for SaaS 2026

Compare Inngest, BullMQ, and Trigger.dev for SaaS jobs in 2026: queues, retries, cron, serverless fit, Redis, and AI workflow observability.

StarterPick Team
Hero image for Inngest vs BullMQ vs Trigger.dev for SaaS 2026

Background Jobs: Required Infrastructure for Production SaaS

Every production SaaS needs background processing: sending emails asynchronously, processing uploaded files, syncing with external APIs, generating reports, retrying webhooks, and running scheduled tasks. These operations should not live inside the user-facing request-response path because retries, timeouts, and partial failures need their own control plane.

If you are comparing BullMQ vs Inngest for a Next.js boilerplate, the decision is mostly about runtime shape: BullMQ is a Redis queue plus always-on workers, while Inngest is a serverless-friendly workflow system with event triggers, steps, retries, and schedules. Trigger.dev sits closer to Inngest, but emphasizes long-running tasks, AI workflows, and run-level observability.

In 2026, the practical shortlist for SaaS boilerplates is:

  • Inngest — durable functions and scheduled workflows that fit serverless Next.js deployments.
  • BullMQ — Redis-backed queues and workers when you control a persistent Node.js process.
  • Trigger.dev — managed tasks for long-running, multi-step, AI-heavy, or observability-sensitive jobs.

TL;DR

  • Choose Inngest when your SaaS starts on Vercel or another serverless platform and you want workflow steps, cron, retries, and event triggers without adding Redis workers.
  • Choose BullMQ when your starter already includes Redis and a persistent worker host, or you need queue-level controls such as worker concurrency, repeatable jobs, job dependencies, and Redis-native operations.
  • Choose Trigger.dev when your product roadmap includes AI/LLM jobs, document-processing pipelines, long-running tasks, or stakeholder-visible run timelines.
  • Avoid choosing only by framework popularity. Match the background-job tool to your deployment model, then check whether the boilerplate already includes the worker/runtime pieces you need.

Key Takeaways

  • Inngest is the best default for serverless-first SaaS boilerplates because it avoids a separate Redis worker process and gives you step-level retries.
  • BullMQ is the best fit when your architecture already has Redis plus an always-on worker on Railway, Fly.io, Render, a VPS, or a container platform.
  • Trigger.dev is the best fit when run visibility matters as much as execution, especially for long-running AI, media, and integration workflows.
  • Cron and schedules are supported by all three, but the operational model differs: Inngest and Trigger.dev invoke managed schedules, while BullMQ repeatable jobs still depend on a running worker process.
  • Price and free-tier limits change. Treat pricing as a final procurement check, not the primary architecture decision.

Inngest: Serverless Background Jobs

Inngest is the background job system to evaluate when you want to avoid running your own Redis queue and worker fleet — your Next.js app exposes an Inngest handler, and Inngest coordinates events, steps, retries, and schedules.

npm install inngest

Setup

// inngest/client.ts
import { Inngest } from 'inngest';

export const inngest = new Inngest({ id: 'my-saas' });
// inngest/functions.ts
import { inngest } from './client';

// A durable background function:
export const processDocument = inngest.createFunction(
  { id: 'process-document', name: 'Process Document Upload' },
  { event: 'document/uploaded' },
  async ({ event, step }) => {
    const { documentId, userId } = event.data;

    // Step 1: Extract text
    const text = await step.run('extract-text', async () => {
      const doc = await db.document.findUnique({ where: { id: documentId } });
      return extractTextFromPdf(doc.url);
    });

    // Step 2: Generate embeddings (if this fails, retries from here)
    const embedding = await step.run('generate-embedding', async () => {
      const { embedding } = await embed({
        model: openai.embedding('text-embedding-3-small'),
        value: text,
      });
      return embedding;
    });

    // Step 3: Store in database
    await step.run('store-embedding', async () => {
      await db.document.update({
        where: { id: documentId },
        data: { embedding, processedAt: new Date() },
      });
    });

    // Step 4: Notify user
    await step.run('notify-user', async () => {
      await sendEmail({
        to: userId,
        subject: 'Document processed',
        body: 'Your document is ready for search.',
      });
    });

    return { success: true };
  }
);

// Scheduled function (cron):
export const dailyDigest = inngest.createFunction(
  { id: 'daily-digest' },
  { cron: '0 9 * * *' },  // 9am daily
  async ({ step }) => {
    const users = await step.run('get-active-users', async () => {
      return db.user.findMany({ where: { emailDigest: true } });
    });

    await step.run('send-digests', async () => {
      await Promise.all(users.map(u => sendDigestEmail(u)));
    });
  }
);
// app/api/inngest/route.ts
import { serve } from 'inngest/next';
import { inngest } from '@/inngest/client';
import { processDocument, dailyDigest } from '@/inngest/functions';

export const { GET, POST, PUT } = serve({
  client: inngest,
  functions: [processDocument, dailyDigest],
});
// Triggering a function from your app:
await inngest.send({
  name: 'document/uploaded',
  data: { documentId, userId },
});

BullMQ: Redis-Backed Performance

BullMQ is the modern job queue built on Redis. It is the fastest option and supports complex job patterns (priorities, rate limiting, job dependencies).

npm install bullmq ioredis
// lib/queue.ts
import { Queue, Worker } from 'bullmq';
import { Redis } from 'ioredis';

const connection = new Redis(process.env.REDIS_URL!, { maxRetriesPerRequest: null });

// Define queues:
export const emailQueue = new Queue('email', { connection });
export const documentQueue = new Queue('document-processing', { connection });

// Define workers (these run in a separate process):
export const emailWorker = new Worker(
  'email',
  async (job) => {
    const { to, subject, body } = job.data;
    await sendEmail({ to, subject, body });
  },
  {
    connection,
    concurrency: 10,  // Process 10 jobs simultaneously
  }
);

export const documentWorker = new Worker(
  'document-processing',
  async (job) => {
    const { documentId } = job.data;
    await processDocumentJob(documentId);
  },
  {
    connection,
    concurrency: 5,
  }
);

// Error handling:
documentWorker.on('failed', (job, err) => {
  console.error(`Job ${job?.id} failed:`, err.message);
  // Send to Sentry, etc.
});
// Adding jobs to the queue:
// From any API route or server action:
await emailQueue.add(
  'welcome-email',
  { to: user.email, subject: 'Welcome!', body: welcomeBody },
  {
    delay: 0,
    attempts: 3,
    backoff: { type: 'exponential', delay: 2000 },
  }
);

// Scheduled job (requires separate cron setup):
await documentQueue.add(
  'process-pending',
  { batchSize: 50 },
  {
    repeat: { pattern: '*/5 * * * *' },  // Every 5 minutes
  }
);

BullMQ requirement: You need a Redis-compatible data store and a worker process that stays online. Upstash Redis can remove Redis server management, but it does not remove the need to run BullMQ workers somewhere outside the request lifecycle.


Trigger.dev: Long-Running AI Jobs

Trigger.dev is built for AI workflows that run for minutes or hours — perfect for LLM pipelines, multi-step automations, and anything that hits third-party APIs.

npm install @trigger.dev/sdk @trigger.dev/nextjs
// trigger/process-document.ts
import { task, logger } from '@trigger.dev/sdk/v3';

export const processDocumentTask = task({
  id: 'process-document',
  maxDuration: 300,  // 5 minutes max
  retry: {
    maxAttempts: 3,
    factor: 2,
    minTimeoutInMs: 1000,
    maxTimeoutInMs: 30000,
  },
  run: async (payload: { documentId: string; userId: string }) => {
    logger.info('Processing document', { documentId: payload.documentId });

    // Step 1: Download document
    const doc = await db.document.findUnique({ where: { id: payload.documentId } });
    const text = await downloadAndExtract(doc.url);

    logger.info('Text extracted', { length: text.length });

    // Step 2: AI analysis (may take 30-60 seconds for large docs)
    const analysis = await analyzeWithAI(text);

    // Step 3: Store results
    await db.document.update({
      where: { id: payload.documentId },
      data: { analysis, processedAt: new Date() },
    });

    // Step 4: Notify user
    await sendEmail({
      to: payload.userId,
      subject: 'Document analysis complete',
    });

    return { success: true, analysisLength: JSON.stringify(analysis).length };
  },
});
// Trigger a task from your API:
import { tasks } from '@trigger.dev/sdk/v3';
import { processDocumentTask } from '@/trigger/process-document';

export async function POST(req: Request) {
  const { documentId } = await req.json();
  const session = await auth();

  const handle = await processDocumentTask.trigger({
    documentId,
    userId: session.user.id,
  });

  return Response.json({ runId: handle.id });
}

Trigger.dev provides a real-time dashboard showing each step, its duration, inputs, outputs, and any errors. This observability is Trigger.dev's key differentiator.


Comparison Table

FeatureInngestBullMQTrigger.dev
InfrastructureNone (serverless)Redis requiredNone (managed)
Serverless compatibleYesPartialYes
Long-running jobsSplit work into retryable stepsControlled by your worker hostDesigned for long-running managed tasks
DashboardYesThird-partyYes (excellent)
Steps/checkpointsYesNoYes
Cron schedulingYesYesYes
Primary runtime dependencyInngest service + API routeRedis + persistent workerTrigger.dev service + task runtime
Pricing caveatCheck current usage tiersRedis + worker hosting costCheck current usage tiers
Best forServerless, simplePerformance, complexAI/LLM, long-running

Which Boilerplates Use What?

BoilerplateBackground Jobs
ShipFastNone (add manually)
OpenSaaS (Wasp)Built-in (PgBoss)
MakerkitInngest (plugin)
AI-heavy templatesOften Trigger.dev or custom orchestration
T3 StackNone (add manually)

Decision Guide

Choose Inngest if:
  → Deploying to Vercel serverless
  → Don't want Redis infrastructure
  → Simple to medium job complexity
  → You want step-level retries and managed schedules

Choose BullMQ if:
  → Already have Redis (Upstash works)
  → Need maximum throughput
  → Complex job topologies (dependencies, priorities)
  → Long-running workers in a separate process

Choose Trigger.dev if:
  → AI/LLM workflows (long-running)
  → Need excellent observability/debugging
  → Multi-step workflows with retry per step
  → Need managed schedules, realtime run status, or per-step logs

Source Notes

Methodology

This refresh keeps the existing canonical StarterPick comparison guide and aligns the recommendation with current official documentation for Inngest, BullMQ, Trigger.dev, Upstash Redis, and Vercel Functions. Volatile pricing and free-tier claims are intentionally treated as procurement checks rather than as architecture evidence.


Common Mistakes in Background Job Architecture

Background job systems introduce a category of bugs that are hard to reproduce and slow to diagnose. The most common patterns in SaaS codebases:

The idempotency problem: a job runs once, fails halfway through, and retries — running the second half twice. If "send welcome email + create Stripe customer" is one job, a failure after the email sends but before the Stripe call means the retry sends a second welcome email. The solution is designing jobs to be idempotent — safe to run multiple times. For Inngest, this means using step.run() for each operation individually, since Inngest's step system checkpoints between steps and retries only from the last failed step. For BullMQ, this means checking whether an operation was already completed before performing it.

The fanout problem: a job that creates N child jobs is a common pattern for batch processing. If the parent job runs successfully but 30% of the child jobs fail, you need visibility into which specific items failed without re-running the whole batch. Trigger.dev's per-step observability handles this naturally. For Inngest and BullMQ, implement explicit tracking of individual item success/failure in your database, not just job-level status.

The long-running job timeout problem: serverless platforms enforce runtime limits, so a request handler is not the right place for a worker that polls forever or runs an unbounded workflow. Inngest handles this by breaking jobs into steps and resuming across function invocations. BullMQ workers run in a persistent process, so their duration depends on the worker host rather than the Next.js request. If your SaaS starts on Vercel and needs jobs that continue outside a request, choose Inngest or Trigger.dev unless you are willing to add a separate worker host.

Observability and Debugging in Production

All three tools differ significantly in their observability story — how easy it is to understand what happened when something goes wrong.

Inngest's dashboard shows function runs with their events, steps, and retry history. When a job fails, you can see exactly which step failed, the input that caused the failure, and the error. Inngest also provides a local development server (Dev Server) that lets you test functions locally with a visual interface before deploying — a significant debugging advantage over BullMQ's more manual local testing.

BullMQ has no built-in dashboard — you add Bull Board or Arena as a third-party dashboard. These dashboards show queue depths, job states (waiting, active, completed, failed), and job data. They work but require separate deployment and configuration. The Redis-native approach means you can also inspect jobs directly with Redis CLI, which can be faster for debugging specific issues.

Trigger.dev's observability is the most detailed of the three: every function run shows a timeline of each step with its duration, input payload, output payload, and any errors. For AI/LLM jobs where each step might be an API call to OpenAI or Anthropic, seeing the exact prompt and response for a failed step is enormously valuable. This observability is Trigger.dev's clearest differentiator.

Scheduling and Cron Job Patterns

All three tools support scheduled jobs (cron), but the implementation patterns and reliability characteristics differ significantly.

Inngest's cron support uses the { cron: '0 9 * * *' } event trigger syntax — the same POSIX cron format used by most cron tools. Inngest executes scheduled functions from its infrastructure, meaning your cron schedule doesn't depend on your Vercel deployment being "warmed up" or a separate process being running. The function receives no event payload (no data is passed to a cron-triggered function), so you fetch the data you need inside the function steps. The Inngest dashboard shows the last run time, next run time, and history of cron job executions — useful for verifying that scheduled jobs are actually running as expected.

BullMQ's repeat/cron feature adds scheduled jobs to the queue: queue.add('job-name', payload, { repeat: { pattern: '0 9 * * *' } }). This works reliably when the worker process is running, but requires that your worker process be always on. For Vercel serverless deployments, this means adding a separate persistent server (Railway, Fly.io, or a VPS) specifically to host BullMQ workers — cron jobs can't run in serverless functions without a persistent process to maintain the schedule.

Trigger.dev scheduled tasks use a different model: you define a schedules.task that Trigger.dev's infrastructure invokes on the configured schedule. This eliminates the need for a persistent process and gives you the same full-run observability for scheduled jobs as for event-triggered jobs. Trigger.dev's scheduling is particularly well-suited for jobs that need to run for more than a few minutes, since the managed infrastructure handles the timeout concerns that affect Vercel-hosted cron solutions.

For most early-stage SaaS on Vercel, Inngest's cron support is the simplest path: no additional infrastructure, runs from the same Next.js deployment, and has adequate observability for monitoring scheduled jobs. If you grow to the point where cron jobs need to run on dedicated persistent infrastructure, BullMQ's feature set becomes more competitive. The practical rule: choose your background job tool based on your deployment infrastructure, not on anticipated future requirements. Inngest for serverless-first, BullMQ when you have or want a persistent process, Trigger.dev when observability and long-running AI workflows are primary concerns.


Which Background Job Tool Belongs in Your SaaS Boilerplate Stack

The decision between Inngest, BullMQ, and Trigger.dev is largely determined by two variables: your deployment target and the complexity of your job workflows.

If you're deploying to Vercel (serverless): BullMQ is the wrong choice unless you add a separate persistent process. BullMQ workers are long-lived Node.js processes, while Vercel Functions are request-scoped runtimes with platform limits. You would need Railway, Fly.io, Render, a VPS, or another worker host specifically for BullMQ. For most early-stage SaaS teams, adding and maintaining that infrastructure is not worth it until queue throughput or Redis-native control becomes a concrete requirement. Inngest and Trigger.dev fit serverless-first projects because they handle persistence and retry orchestration in their own infrastructure while your Next.js app exposes handlers or triggers tasks.

If you have a persistent server (Node.js on Railway, Fly.io, Render): All three tools work well. BullMQ becomes a serious option because you can run long-lived workers without serverless timeout concerns. The Redis dependency is manageable — Upstash Redis provides a managed Redis-compatible store without a self-managed server. At this point, the decision shifts to observability and workflow complexity: BullMQ for maximum control and Redis-native integration, Inngest for developer experience and local testing tooling, Trigger.dev for AI workloads with detailed per-step observability.

If you're building AI-heavy workflows: Trigger.dev's per-step execution model directly addresses the most common AI job failure mode: LLM API calls that timeout, return errors, or need to be retried with different inputs. Running a RAG pipeline (document ingestion → chunking → embedding → vector storage → index update) as a Trigger.dev function gives you visibility into exactly which step failed and what the LLM returned, rather than just "job failed at step 3." For SaaS products where AI pipelines are core functionality, this observability is worth the added dependency.

For boilerplate-first SaaS projects: The simplest path is to choose the tool your starter already wires correctly, then verify that its runtime model matches your deployment. A starter with Inngest handlers is easier to launch on Vercel than one that requires provisioning Redis workers. A starter with BullMQ can be the right choice when it also ships a documented worker process and deployment target. A starter with Trigger.dev is attractive when the product roadmap includes AI tasks, media processing, or integrations where run timelines and step logs are product-support requirements. The switching cost between tools is meaningful — job definitions, retry logic, and observability configuration are tool-specific — so choose the background job tool that matches your deployment architecture from the start, and plan to stay with it unless a concrete requirement forces a change.


Building a SaaS with background jobs? StarterPick helps you find boilerplates pre-configured with the right job infrastructure for your needs.

The background job tool you choose becomes part of your production operations — monitoring, alerting, and debugging workflows all depend on what observability the tool provides. Choose based on your deployment architecture first, and your observability requirements second.

See background jobs in context of the full SaaS stack: Ideal tech stack for SaaS in 2026.

Find boilerplates pre-configured with job infrastructure: Best SaaS boilerplates 2026.

Compare AI-ready boilerplates that use Trigger.dev for LLM workflows: Best boilerplates for AI wrapper SaaS 2026.

Find the right background job infrastructure in your stack: Next.js SaaS tech stack guide 2026.

The SaaS Boilerplate Matrix (Free PDF)

20+ SaaS starters compared: pricing, tech stack, auth, payments, and what you actually ship with. Updated monthly. Used by 150+ founders.

Join 150+ SaaS founders. Unsubscribe in one click.