EmpireUI
Get Pro
← Blog9 min read#trigger.dev#inngest#background jobs

Trigger.dev vs Inngest: Background Jobs for Next.js Compared

Trigger.dev and Inngest both promise durable background jobs in Next.js — but they make very different tradeoffs. Here's the real-world breakdown.

Developer laptop showing code editor with background job queue dashboard

Why Background Jobs in Next.js Are Still Painful in 2026

You'd think this problem would be solved by now. It's 2026, Vercel has a dozen edge primitives, and yet shipping reliable background jobs in a Next.js app is still one of those things that bites teams hard enough to rewrite. The core issue is that serverless functions are stateless and short-lived — most providers cap execution at 5 to 15 minutes, and there's no built-in way to survive a timeout, retry a failed step, or fan out work across 10 parallel tasks without writing a lot of glue code.

For a long time, the go-to answer was 'spin up a separate worker process' — a Redis-backed Bull queue on a VPS, a small Express server, whatever. It works. But it's ops overhead, and if you're on a team that went all-in on serverless to avoid exactly that, it feels like a step backward. Trigger.dev and Inngest both offer a different promise: durable job execution that hooks directly into your Next.js codebase, no separate infra required.

Worth noting: these two tools have converged a lot over the last 18 months. Both support retries, delays, fan-out, and event-driven workflows. The differences that matter are in the developer experience, pricing model, and the mental model each tool asks you to adopt. That's what this comparison is actually about.

Trigger.dev: The Mental Model

Trigger.dev v3 (released late 2024) made a sharp pivot. It moved away from the YAML-config webhook approach of v1 and fully embraced a code-first SDK where your tasks are just TypeScript functions decorated with a task() wrapper. The runtime handles durability — it checkpoints execution state so a task can pause mid-run, wait days, and resume on a different worker without losing context.

The key concept is tasks. You define a task in a file, export it, and Trigger's worker process picks it up. You trigger it from your Next.js route handlers or server actions by calling tasks.trigger(). Dead simple at the surface. The power comes from ctx.run(), ctx.wait(), and the ability to call wait.for({ seconds: 86400 }) to pause for 24 hours without holding a serverless function open.

// trigger/send-onboarding-email.ts
import { task } from '@trigger.dev/sdk/v3';
import { sendEmail } from '../lib/email';

export const onboardingEmail = task({
  id: 'onboarding-email',
  retry: { maxAttempts: 3, factor: 2 },
  run: async (payload: { userId: string; email: string }) => {
    await sendEmail({
      to: payload.email,
      template: 'welcome',
      data: { userId: payload.userId },
    });
    return { sent: true };
  },
});

Then from a Next.js route or server action: ``typescript // app/api/signup/route.ts import { onboardingEmail } from '../../trigger/send-onboarding-email'; export async function POST(req: Request) { const body = await req.json(); // ... create user await onboardingEmail.trigger({ userId: user.id, email: body.email }); return Response.json({ ok: true }); } ``

In practice, Trigger.dev feels like writing normal TypeScript. The SDK stays out of your way. Where it gets interesting — and a bit more complex — is when you start building multi-step workflows with ctx.run() sub-tasks and want to observe execution in the dashboard. The dashboard is excellent, honestly one of the best execution-trace UIs in this space.

Inngest: The Mental Model

Inngest's mental model is event-driven first. You define functions that react to events. An event is just a JSON object with a name and data. You send events from anywhere — a webhook, a route handler, a cron — and Inngest routes them to every function subscribed to that event name. This fan-out is built-in and trivial, which is Inngest's strongest card.

The step API is where Inngest really differentiates. step.run() wraps a unit of work that gets checkpointed automatically. step.sleep() pauses without holding a connection. step.waitForEvent() lets a function literally pause mid-execution and wait for a different event to arrive — say, a user confirming their email — before continuing. That's not something you'd build yourself in a weekend.

// inngest/functions/onboarding-sequence.ts
import { inngest } from '../client';
import { sendEmail } from '../../lib/email';

export const onboardingSequence = inngest.createFunction(
  { id: 'onboarding-sequence', retries: 3 },
  { event: 'user/signed-up' },
  async ({ event, step }) => {
    await step.run('send-welcome', async () => {
      await sendEmail({ to: event.data.email, template: 'welcome' });
    });

    await step.sleep('wait-3-days', '3 days');

    await step.run('send-tips', async () => {
      await sendEmail({ to: event.data.email, template: 'tips' });
    });
  }
);

Look, that step.sleep('wait-3-days', '3 days') call is borderline magic. Your Next.js function returns, Inngest stores state, and three days later it resumes right after that line. No cron job. No database polling. No queue. This is genuinely difficult to replicate without a tool like Inngest or Trigger.dev.

Sending the triggering event from a Next.js route is similarly clean: ``typescript // app/api/signup/route.ts import { inngest } from '../../inngest/client'; export async function POST(req: Request) { const body = await req.json(); // ... create user await inngest.send({ name: 'user/signed-up', data: { email: body.email } }); return Response.json({ ok: true }); } `` One event, potentially multiple functions react. That's the Inngest way.

Head-to-Head: Developer Experience, Local Dev, and Observability

Local development is where the two tools diverge most noticeably. Inngest ships inngest-cli — run npx inngest-cli dev and you get a local server at http://localhost:8288 that proxies events to your Next.js dev server, shows you a full function trace, and lets you replay events. It's fast and zero-config once you have the SDK wired up. Quick aside: you don't need a Docker container, no Redis, nothing external.

Trigger.dev's local story changed in v3. You run npx trigger.dev@latest dev, which starts a local worker process that connects to Trigger's cloud via a tunnel (similar to how ngrok works). That means you need internet access for local development — a bit annoying on a plane. They're aware of this and a fully offline dev mode has been on the roadmap since early 2026, but as of the time of writing it's not shipped.

Observability is a wash — both dashboards are genuinely good. Trigger's run traces show individual step timing with sub-millisecond resolution and let you re-trigger a specific payload right from the UI. Inngest's Function Runs view groups everything by event and shows the full step waterfall. If anything, Inngest's UI is slightly more polished at the event-correlation level because the event-first model makes it natural to trace 'this event triggered these three functions.'

TypeScript types are excellent in both SDKs. Inngest auto-infers the event.data type from your event schema definition. Trigger infers payload types from the task() signature. You won't be fighting any types in either library — a low bar but one that older queue libraries consistently fail.

Pricing, Scale, and Self-Hosting

This is where the comparison gets more consequential, especially if you're building something high-volume. Both tools offer free tiers — Inngest's free plan includes 50,000 function runs per month, Trigger.dev's gives you 50,000 task runs. At small scale you won't pay anything. At medium scale (say, 1–2 million runs/month) both tools cost roughly $50–$150/month depending on concurrency needs. The pricing is close enough that it shouldn't be your deciding factor.

Self-hosting is a different story. Trigger.dev v3 is fully open-source (Apache 2.0) and ships a self-hosted Helm chart and Docker Compose setup. If you need your job data to stay in your own infrastructure — regulated industries, strict data residency requirements — Trigger.dev is the obvious choice. Inngest's self-hosted offering exists but is gated behind their enterprise plan and isn't openly documented. Honestly, if self-hosting is a hard requirement, Trigger.dev wins this category without contest.

One more thing — concurrency limits. Trigger.dev v3 lets you set per-task concurrency at the code level with concurrency: { limit: 10 }. Inngest has function-level concurrency and also supports throttling (rate-limiting how fast a function can start), which is handy when you're hammering a third-party API that has its own rate limits. That throttle primitive is genuinely useful and Trigger.dev's equivalent is less ergonomic.

// Inngest throttle example — max 100 calls per minute to a rate-limited API
export const processInvoice = inngest.createFunction(
  {
    id: 'process-invoice',
    throttle: { count: 100, period: '1m', key: 'event.data.accountId' },
  },
  { event: 'invoice/created' },
  async ({ event, step }) => {
    // safe to call a rate-limited payment API here
  }
);

When to Pick Trigger.dev

Pick Trigger.dev when your workflows are task-centric rather than event-centric. If you're building something like 'process this uploaded PDF,' 'run this nightly data export,' or 'send this email after a delay' — the explicit task.trigger() call is more intuitive than thinking in events. It maps 1:1 to how most teams already think about background work.

The self-hosting angle matters a lot for certain teams. If you're building a B2B SaaS that processes financial data, medical records, or anything under GDPR with strict data residency, being able to run Trigger.dev entirely on your own infra is a meaningful advantage. And since it's Apache 2.0, you can fork it, read the internals, and understand exactly what's happening to your job data.

Trigger.dev also wins on the long-running machine workload side. Their v3 runtime supports tasks that run for hours, not minutes — useful for ML inference jobs, video processing, large file uploads. Serverless functions cap you. Trigger.dev's worker model doesn't. If you're building anything that needs to process a 2GB CSV or run a batch ML pipeline, the Trigger model fits better.

That said, if offline local dev matters to your team and you have poor internet at your dev location (or a strictly air-gapped dev environment), factor in Trigger's current cloud-tunnel dependency before committing. It's not a dealbreaker but it's a real friction point.

When to Pick Inngest

Inngest shines when your system is already event-driven or when you want to move it that direction. User signed up, invoice paid, webhook received — if your domain naturally produces events, Inngest's fan-out model means you can add a new behavior (send a Slack notification, kick off a compliance check, update a search index) by adding a new function without touching any existing code. The decoupling is real.

The step.waitForEvent() primitive is Inngest's killer feature with no clean Trigger.dev equivalent. Pause a workflow and resume it when a human confirms something, when a webhook fires, when another process signals completion — you can model sophisticated async business processes without a state machine library. For anything resembling an approval flow or a multi-step checkout, this API is incredibly expressive.

Inngest's local development experience is genuinely better right now. If you want to spin something up quickly, see a beautiful local UI immediately, and not deal with tunnel configuration, inngest-cli dev gets you there in under 60 seconds. For teams prototyping fast or doing demos, that first-hour experience matters.

Honestly, the Inngest community has also invested heavily in official integration guides for the most common Next.js patterns — Clerk webhooks, Stripe events, Resend callbacks. If you're using that standard stack, you'll find copy-paste examples for your exact setup. Empire UI components pair naturally with that Inngest + Next.js stack if you're building a polished UI on top of these workflows — check the templates for production-ready starting points that already handle the frontend side.

The Verdict

There's no universally correct answer here, but there is a pattern. Inngest is the better default for most Next.js SaaS apps being built today — the local dev experience is smoother, the event model encourages better architecture as your app grows, and step.waitForEvent() covers business logic that's otherwise painful to implement. Start with Inngest unless you have a specific reason not to.

Switch to Trigger.dev if you need self-hosting with full control, if your workloads are genuinely long-running (think minutes to hours, not seconds), or if your team thinks in tasks rather than events and wants an API that matches that mental model. The v3 SDK is mature, the dashboard is excellent, and the open-source commitment means you're not locked into a vendor's pricing decisions.

Worth noting: both tools are actively maintained with multiple releases per month as of mid-2026. You're not betting on abandonware with either choice. Pick based on DX fit and architectural alignment, not on which team has more GitHub stars this week.

If you're building the frontend to sit on top of these workflows — progress indicators, admin dashboards, job status UIs — browse components on Empire UI to skip the boilerplate. The glassmorphism components in particular work well for status overlay UIs where you want visual depth without heavy UI libraries weighing down your bundle.

FAQ

Can I use Trigger.dev or Inngest with Next.js App Router and Server Actions?

Yes, both work with App Router. You call task.trigger() or inngest.send() from server actions or route handlers — they're just async function calls. Neither library requires any special App Router configuration beyond the standard API route setup.

Do these tools work on Vercel's free plan?

Both tools work on Vercel, but your Next.js functions still hit Vercel's timeout limits (10s on Hobby, 60s on Pro). The job runners themselves live outside Vercel — on Trigger.dev's or Inngest's infrastructure — so the actual task execution isn't constrained by Vercel's limits. Your trigger call just needs to complete within the timeout.

Is there a meaningful performance difference between the two?

Not at typical SaaS scale. Both tools add roughly 100–300ms of overhead per job invocation compared to in-process execution. At hundreds of thousands of runs per day you'd want to benchmark your specific workload, but for most apps this latency is irrelevant.

What happens if my job fails — do I lose data?

Both platforms checkpoint state and retry automatically on failure. With Inngest, each step.run() call is individually retried without re-running earlier steps. Trigger.dev v3 does the same with its sub-task checkpointing. You won't replay a charge twice as long as you wrap side effects in the appropriate step primitive.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Next.js vs Remix in 2026: Which One Should You Use?Vite + React vs Next.js in 2026: Which Scaffold to ChooseTrigger.dev Guide: Background Jobs, Schedules and Retries in Next.jsInngest Guide: Event-Driven Functions and Workflows in Next.js