Background Jobs in SaaS: Inngest vs BullMQ vs Trigger.dev in 2026

# inngest# bullmq# triggerdev# backgroundjobs
Background Jobs in SaaS: Inngest vs BullMQ vs Trigger.dev in 2026Royce

TL;DR Inngest for Vercel-hosted Next.js apps — serverless-native, no Redis required, great...

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
Enter fullscreen mode Exit fullscreen mode

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


// 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 />,
        });
      });
    }
  }
);
Enter fullscreen mode Exit fullscreen mode
// app/api/inngest/route.ts — Inngest handler

  client: inngest,
  functions: [sendWelcomeEmail],
});
Enter fullscreen mode Exit fullscreen mode
// Trigger from API route — fire and forget
await inngest.send({ name: 'user/signed.up', data: { userId: user.id } });
Enter fullscreen mode Exit fullscreen mode

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() and step.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

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
);
Enter fullscreen mode Exit fullscreen mode

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

  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);
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

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.