Inngest vs Temporal vs Trigger.dev 2026
The Background Jobs Problem
Modern applications need to run code that doesn't fit in a request/response cycle: send emails after signup, process uploaded files, run nightly data syncs, retry failed webhook deliveries, orchestrate multi-step AI pipelines. Serverless architectures made this harder — no long-running processes, functions time out, infrastructure is ephemeral.
Durable execution platforms solve this. They let you write code that looks sequential but runs reliably across failures, restarts, and retries — without managing queues, workers, or retry state yourself.
In 2026, three platforms dominate: Inngest, Temporal, and Trigger.dev. They share the same core promise but take very different approaches in complexity, target audience, and operational model.
TL;DR
- Inngest: Best for TypeScript/serverless teams wanting minimal infrastructure. Excellent Next.js/Vercel integration. Event-driven architecture makes it natural for product workflows.
- Temporal: Best for complex, long-running business workflows with strict reliability requirements. More complex to operate, more powerful state machines. The enterprise choice.
- Trigger.dev: Best for TypeScript developers wanting background jobs that feel like writing normal code. v3 architecture is the most modern developer experience of the three.
Key Takeaways
| Inngest | Temporal | Trigger.dev | |
|---|---|---|---|
| Primary model | Event-driven steps | Workflow/activity | Background tasks |
| Best fit | Serverless/Next.js | Enterprise workflows | TypeScript apps |
| Self-host | ✅ | ✅ (primary) | ✅ |
| Cloud | ✅ | ✅ (Temporal Cloud) | ✅ |
| Free tier | ✅ | Limited trial | ✅ |
| SDK languages | TS, Python, Go | TS, Go, Python, Java, .NET | TypeScript (Python via extension) |
| Complexity | Low | High | Low |
Inngest
What It Is
Inngest is a durable execution platform built for serverless and edge environments. Your functions are deployed as HTTP endpoints (serverless functions, API routes) — Inngest calls them, tracks state, handles retries, and manages the execution flow. You don't run workers.
This is fundamentally different from Temporal: there's no long-running process to manage. Inngest works with Vercel, Netlify, AWS Lambda, and any HTTP-capable runtime.
Core Concepts
Functions: Your business logic, decorated with Inngest metadata:
import { inngest } from "./client";
export const processUserSignup = inngest.createFunction(
{
id: "process-user-signup",
retries: 3,
},
{ event: "user/signed.up" },
async ({ event, step }) => {
// Step 1: Send welcome email
await step.run("send-welcome-email", async () => {
await sendEmail(event.data.email, "Welcome!");
});
// Step 2: Wait 3 days, then send onboarding email
await step.sleep("wait-for-onboarding", "3 days");
await step.run("send-onboarding-email", async () => {
await sendEmail(event.data.email, "How's it going?");
});
// Step 3: Create trial reminder after 14 days
await step.sleep("wait-for-trial-reminder", "11 days");
await step.run("send-trial-ending-email", async () => {
const user = await db.users.findById(event.data.userId);
if (!user.isPaid) {
await sendEmail(event.data.email, "Your trial is ending!");
}
});
}
);
Each step.run() is a checkpoint — if the function fails mid-way, Inngest resumes from the last completed step, not from the beginning. The step.sleep() doesn't block a worker; Inngest schedules the next wake-up and invokes your function again.
Events: Triggers that start or fan-out to functions:
// Trigger the function from anywhere in your app
await inngest.send({
name: "user/signed.up",
data: { userId: "usr_123", email: "user@example.com" },
});
Fan-out: One event triggers multiple functions in parallel:
// Multiple functions can listen to the same event
export const syncToHubspot = inngest.createFunction(
{ id: "sync-hubspot" },
{ event: "user/signed.up" },
async ({ event, step }) => { /* ... */ }
);
export const createStripeCustomer = inngest.createFunction(
{ id: "create-stripe-customer" },
{ event: "user/signed.up" },
async ({ event, step }) => { /* ... */ }
);
Both fire simultaneously when user/signed.up is sent.
Cron Jobs
export const dailyDigest = inngest.createFunction(
{ id: "daily-digest" },
{ cron: "0 9 * * *" }, // 9am UTC daily
async ({ step }) => {
const users = await step.run("fetch-active-users", fetchActiveUsers);
for (const batch of chunk(users, 50)) {
await step.run(`send-digest-${batch[0].id}`, () => sendDigestBatch(batch));
}
}
);
Next.js Integration
// app/api/inngest/route.ts
import { serve } from "inngest/next";
import { inngest } from "@/inngest/client";
import { processUserSignup, dailyDigest } from "@/inngest/functions";
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [processUserSignup, dailyDigest],
});
That's the full integration. Inngest discovers your functions via this endpoint, registers them, and starts routing events.
Pricing
- Free: 50K–100K function runs/month; 50 concurrent runs; event history
- Pro (~$350/month): 200 concurrent steps; granular metrics; increased throughput; 14-day trace retention; priority support; $50 per 1M additional executions
- Enterprise: Custom; SAML/RBAC; audit trails; dedicated Slack; 90-day trace retention; SLAs
Inngest charges per function run (each step.run() call counts). The pricing is predictable for typical workloads.
Self-Hosting
Inngest's server is source-available under SSPL (Server Side Public License) with a delayed open-source arrangement — code is relicensed to Apache 2.0 after a 3-year delay. Self-host with Docker:
docker run -p 8288:8288 inngest/inngest:latest dev
The cloud product adds managed infrastructure, team features, and SLAs.
Temporal
What It Is
Temporal is a workflow orchestration engine built for mission-critical, long-running processes. It originated at Uber (as Cadence) and is designed for workflows that span hours, days, or years — financial transactions, order fulfillment, data pipelines, compliance processes.
Temporal's model is fundamentally different from Inngest: you write workflows (long-running code) and activities (individual steps), and Temporal's server ensures they execute exactly-once, with full state persistence and history.
Core Concepts
Workflows: Deterministic functions that orchestrate activities:
import { proxyActivities, sleep, defineWorkflow } from "@temporalio/workflow";
import type * as activities from "./activities";
const { sendEmail, createSubscription, chargeCard } = proxyActivities<typeof activities>({
startToCloseTimeout: "30 seconds",
retry: {
maximumAttempts: 3,
initialInterval: "1 second",
backoffCoefficient: 2,
},
});
export async function userOnboardingWorkflow(userId: string): Promise<void> {
// Send welcome email
await sendEmail(userId, "welcome");
// Create subscription (idempotent)
await createSubscription(userId);
// Wait 14 days for trial period
await sleep("14 days");
// Charge for first subscription
const result = await chargeCard(userId, "monthly");
if (result.failed) {
await sendEmail(userId, "payment-failed");
}
}
Activities: Individual units of work (I/O, external API calls):
// activities.ts
export async function sendEmail(userId: string, template: string): Promise<void> {
const user = await db.users.findById(userId);
await emailProvider.send(user.email, template);
}
export async function chargeCard(userId: string, plan: string): Promise<{ failed: boolean }> {
try {
await stripe.charge(userId, plan);
return { failed: false };
} catch {
return { failed: true };
}
}
Workers: Long-running processes that execute workflows and activities:
// worker.ts
import { Worker } from "@temporalio/worker";
import * as activities from "./activities";
const worker = await Worker.create({
workflowsPath: require.resolve("./workflows"),
activities,
taskQueue: "user-onboarding",
});
await worker.run(); // Runs indefinitely
This is Temporal's key operational requirement: you must run workers. No workers → no workflow execution. This makes Temporal less compatible with serverless environments (though workarounds exist).
Signals and Queries
Temporal's killer feature for complex workflows: external code can interact with a running workflow:
// In the workflow definition
import { setHandler, defineSignal, defineQuery } from "@temporalio/workflow";
const approveSignal = defineSignal<[string]>("approve");
const statusQuery = defineQuery<string>("status");
export async function approvalWorkflow(): Promise<void> {
let approved = false;
let approver = "";
setHandler(approveSignal, (approvedBy) => {
approved = true;
approver = approvedBy;
});
setHandler(statusQuery, () => approved ? `Approved by ${approver}` : "Pending");
// Wait indefinitely for approval (can be years)
await condition(() => approved, "365 days");
await processAfterApproval(approver);
}
// External code sends the signal
await client.workflow.getHandle(workflowId).signal("approve", "manager@company.com");
// Query current status
const status = await client.workflow.getHandle(workflowId).query("status");
This pattern — human-in-the-loop, multi-party approval, async notification handling — is extremely difficult in other systems and natural in Temporal.
Pricing
Temporal Cloud:
- Action rate: $50 per 1M actions (raised from $25/1M in early 2025); volume discounts from 5M+ actions
- Minimum spend: Greater of $100/month or 5% of monthly consumption
- Active workflow storage: $1/GB-day; closed workflow history: $0.01/GB-day
- Default capacity: 500 Actions/second per namespace; add Temporal Resource Units (TRUs) for more
- Payment: pay-as-you-go (monthly invoice) or pre-purchased Temporal Credits
- Enterprise: Custom; dedicated support
Self-hosted: Free (Apache 2.0), but you pay infrastructure (Temporal server + database + workers). Non-trivial to operate at scale.
The operational complexity and pricing model make Temporal most suitable for teams with engineering capacity to maintain it — or enterprises paying for Temporal Cloud.
Trigger.dev
What It Is
Trigger.dev is the newest of the three, designed from the ground up for the modern TypeScript developer. v3 (2024) rearchitected the platform around durable tasks — functions that run reliably, resume after failures, and integrate with modern TS tooling.
The pitch: background jobs should feel like writing normal code, not operating distributed systems infrastructure.
Core Concepts
Tasks: The fundamental unit — async functions tagged with Trigger.dev metadata:
import { task, wait, logger } from "@trigger.dev/sdk/v3";
export const processDocument = task({
id: "process-document",
retry: {
maxAttempts: 3,
minTimeoutInMs: 1000,
factor: 2,
},
run: async (payload: { documentId: string }) => {
const doc = await fetchDocument(payload.documentId);
// Extract text
const text = await extractText(doc);
// Wait for external processing (doesn't block a worker)
const result = await wait.for({ seconds: 30 });
// Run AI classification
const classification = await classifyDocument(text);
logger.info("Document classified", { classification });
return { classification };
},
});
Triggering tasks:
// From your API or anywhere in your app
import { tasks } from "@trigger.dev/sdk/v3";
// Trigger and get back a handle
const handle = await tasks.trigger("process-document", { documentId: "doc_123" });
// Or trigger and wait for result
const result = await tasks.triggerAndWait("process-document", { documentId: "doc_123" });
Scheduled tasks:
import { schedules } from "@trigger.dev/sdk/v3";
export const weeklyReport = schedules.task({
id: "weekly-report",
cron: "0 9 * * MON",
run: async () => {
const data = await generateWeeklyReport();
await sendReport(data);
},
});
Batch operations:
export const processImages = task({
id: "process-images",
run: async (payload: { imageIds: string[] }) => {
// Process all images in parallel, with automatic batching
const results = await Promise.all(
payload.imageIds.map((id) =>
tasks.triggerAndWait("resize-image", { imageId: id })
)
);
return results;
},
});
AI Task Integration
Trigger.dev has invested in first-class AI workflow support — particularly for long-running AI tasks that exceed serverless timeouts:
import { task } from "@trigger.dev/sdk/v3";
import Anthropic from "@anthropic-ai/sdk";
export const analyzeDocument = task({
id: "analyze-document",
// No timeout worries — Trigger.dev handles long-running tasks
run: async (payload: { text: string }) => {
const client = new Anthropic();
// Claude call that might take 30-60 seconds
const response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 4096,
messages: [{ role: "user", content: `Analyze this document: ${payload.text}` }],
});
return response.content[0].text;
},
});
Pricing
Trigger.dev v3 uses compute-time billing (pay per second of CPU) rather than per-run pricing:
- Free ($0/month): $5 monthly compute credit; 10 concurrent runs; unlimited tasks; 5 team members; 10 schedules; 1-day log retention
- Hobby ($10/month): $10 monthly compute; 25 concurrent runs; 100 schedules; 7-day logs
- Pro ($50/month): $50 monthly compute; 100+ concurrent runs; 25+ team members; 1,000+ schedules; 30-day logs; dedicated Slack support
- Compute usage: Micro: $0.0000169/sec; Large: $0.00068/sec; $0.000025 per run invocation
- Enterprise: Custom; self-hosted option available
The compute-time model makes costs predictable for long-running tasks — you pay for actual CPU time, not per execution count.
Self-Hosting
Trigger.dev is open-source (Apache 2.0). Docker Compose deployment:
git clone https://github.com/triggerdotdev/trigger.dev
cd trigger.dev
cp .env.example .env
docker-compose up
Head-to-Head Comparison
Developer Experience
| Aspect | Inngest | Temporal | Trigger.dev |
|---|---|---|---|
| TypeScript DX | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Learning curve | Low | High | Low |
| Local dev experience | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| Serverless support | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
| Worker management needed | ❌ | ✅ | ❌ |
Features
| Feature | Inngest | Temporal | Trigger.dev |
|---|---|---|---|
| Retries + backoff | ✅ | ✅ | ✅ |
| Step checkpointing | ✅ | ✅ | ✅ |
| Cron/scheduled jobs | ✅ | ✅ | ✅ |
| Fan-out / parallel | ✅ | ✅ | ✅ |
| Sleep / delays | ✅ | ✅ | ✅ |
| Signals/queries | ❌ | ✅ | ❌ |
| Human-in-the-loop | Limited | ✅ | Limited |
| Versioning workflows | ❌ | ✅ | ❌ |
| Multi-language support | TS, Python, Go | 7 languages | TS-first (Python extension) |
When to Use Each
Choose Inngest if:
- You're on Vercel, Netlify, or any serverless stack
- You want event-driven architecture for product workflows
- You have TypeScript, Python, or Go
- You want minimal operational overhead
- Your workflows are product-facing (onboarding, notifications, billing)
Choose Temporal if:
- You have complex, multi-step business workflows with strict requirements
- You need signals/queries for external interaction with running workflows
- You have engineering capacity to operate workers and the Temporal server
- You're building in a regulated industry (finance, healthcare, logistics)
- You need workflow versioning for long-lived workflows (months/years)
Choose Trigger.dev if:
- You're building TypeScript applications and want the cleanest DX
- You have AI/LLM tasks that exceed serverless timeouts
- You want aggressive pricing for high-volume workloads
- You want a modern, actively-developed alternative to Inngest
The Bottom Line
All three solve background job reliability. The differences are about operational model and complexity ceiling:
- Inngest is the fastest way to add durable execution to a Next.js or serverless app. Zero worker management.
- Temporal is the most powerful but requires the most engineering investment. Right for genuinely complex workflows.
- Trigger.dev has the best developer experience in 2026 for pure TypeScript teams, especially for AI workloads.
For a startup that hasn't committed to one: start with Trigger.dev or Inngest. Graduate to Temporal if you outgrow them.
Compare workflow APIs at APIScout.
Related: Stripe vs PayPal vs Adyen vs Square 2026 · OpenAI Agents SDK: Architecture Patterns