Inngest: What It Is and How It Fits In
You now have solid mental models for events, queues, and workers. You know why event-driven architecture exists and what the core machinery looks like.
Now here's the honest question before we go any further: why Inngest specifically?
There are dozens of tools in this space. BullMQ, Celery, Sidekiq, RabbitMQ, Kafka, Temporal, AWS Step Functions, Trigger.dev — they all solve some version of "I need work to happen in the background reliably." What's Inngest's actual position in that landscape? Is it just another job queue with better marketing? Or is it genuinely different?
Let's find out. By the end of this article you'll understand exactly what Inngest is, what problem it's specifically designed to solve, how its mental model works, and — crucially — when you'd reach for it over other tools.
No installation yet. No SDK calls. Just understanding. The code comes in Article 4.
Quick Reference
What Inngest is: An event-driven durable execution platform — it combines a queue, a workflow engine, and observability into a single hosted service that you connect to your existing app over HTTP.
Core mental model: Events trigger functions. Functions are composed of steps. Steps are checkpointed, independently retryable units of work.
What makes it different: Your code runs in your app. Inngest orchestrates it remotely. No long-running servers. No separate worker processes. No Redis to manage.
Supported languages: TypeScript/JavaScript, Python, Go.
What You Need to Know First
Required reading (in order):
You should understand:
- What an event is and why it exists
- How queues buffer work between producers and workers
- What at-least-once delivery means and why idempotency matters
What We'll Cover in This Article
By the end of this guide, you'll understand:
- Where Inngest sits in the landscape of async processing tools
- The three problems Inngest is designed to solve
- Inngest's core mental model: events → functions → steps
- What "durable execution" means and why it matters
- How Inngest's architecture works (your code vs. Inngest's infrastructure)
- When to use Inngest vs. other tools
- What Inngest does NOT do (important to know upfront)
What We'll Explain Along the Way
- Durable execution (the core concept behind Inngest's design)
- Memoization (how completed steps are remembered across retries)
- Flow control (concurrency, throttling, rate limiting)
- The Dev Server (how local development works — expanded in Article 11)
The Landscape: Where Does Inngest Fit?
Let's place Inngest on a map. In Article 2, we introduced this spectrum:
Raw message passing ←————————————————→ Full workflow engine
(Kafka, SQS) Job queues (Inngest, Temporal)
(BullMQ, Celery)
But the spectrum is actually two-dimensional. Tools also differ on how much infrastructure you need to run them:
HIGH INFRA OVERHEAD
▲
│
Temporal ──────────────┤
(self-hosted cluster │
or Temporal Cloud) │
│
Kafka ─────────────────┤
(brokers, ZooKeeper, │
consumer groups) │
│
RabbitMQ ──────────────┤
│
BullMQ ────────────────┤ ← Redis required, Node.js only
│
AWS Step Functions ────┤ ← AWS-only, JSON config
│
Inngest ───────────────┤ ← Managed service, HTTP-based
│
LOW INFRA OVERHEAD
Inngest sits in an interesting position: it has the capabilities of a full workflow engine like Temporal, but the operational simplicity closer to a managed service. As Inngest's documentation describes it: "an event-driven durable execution platform that allows you to run fast, reliable code on any platform, without managing queues, infra, or state."
But what does that actually mean in practice? Let's look at the three specific problems Inngest is designed to solve.
The Three Problems Inngest Solves
Problem 1: The infrastructure tax
Every async tool in the traditional ecosystem requires you to run something alongside your app. BullMQ needs Redis. Celery needs a broker (usually Redis or RabbitMQ). Temporal needs its own server cluster. RabbitMQ is its own deployment. These are real operational costs — more services to provision, monitor, scale, and pay for.
Inngest's comparison to Kafka describes this bluntly: traditional tools "often demand extra effort or tooling to handle scalable queuing efficiently" due to "limited features, complex setup, and scalability challenges."
Inngest's approach is different. Your app connects to Inngest's hosted platform over HTTPS. You don't run any Inngest infrastructure yourself. There's no Redis server to provision. There's no worker process to manage. Your app just needs an HTTP endpoint that Inngest can call.
This matters especially if you're deploying to serverless platforms (Vercel, Netlify, Cloudflare Workers, AWS Lambda). Traditional workers expect a long-running process. Serverless functions are stateless and ephemeral. Inngest is designed from the ground up for this model.
Problem 2: The durability gap
Here's a problem that most job queues solve only partially.
Imagine a three-step workflow: send an email, charge a card, update a database. You wrap all three in a single worker function. The worker executes:
Step 1: Send email ✅
Step 2: Charge card ✅
Step 3: Update database ❌ ← crashes here
What happens on retry? The entire function runs again from the top:
Step 1: Send email again ← user gets a duplicate email 😬
Step 2: Charge card again ← user gets charged twice 🚨
Step 3: Update database ✅
This is the durability gap: most queues retry functions, not steps. To avoid the double-charge problem, you'd need to hand-code idempotency checks at every step — which is exactly what every experienced backend developer dreads.
Inngest solves this with durable execution: each step's result is saved after it succeeds. On retry, completed steps are skipped and their cached results are replayed. The function resumes from exactly where it failed.
First run:
Step 1: Send email ✅ (result saved)
Step 2: Charge card ✅ (result saved)
Step 3: Update database ❌ (fails)
Retry:
Step 1: Send email → SKIPPED (cached result replayed)
Step 2: Charge card → SKIPPED (cached result replayed)
Step 3: Update database ✅ (retried and succeeds)
No duplicate emails. No double charges. This isn't magic — it's a well-defined execution model. Inngest's blog on durable execution principles describes the three building blocks: incremental execution (steps run one at a time), state persistence (each step's result is saved externally), and fault tolerance (failures restart from the last successful step).
Problem 3: The observability void
You've deployed a background job system. Something broke. Now what?
With most queue systems, debugging means grepping logs across multiple services, correlating timestamps, and hoping you can reconstruct what happened. There's no single place that shows you "here's this particular job's execution history, step by step."
Inngest includes a built-in dashboard that gives you a visual timeline of every function run — what triggered it, which steps ran, how long each step took, what errors occurred, and what the retry history looks like. You can replay a failed run in one click after fixing the bug.
This isn't just nice to have. For production systems, observability is the difference between a 5-minute fix and a 3-hour debugging session. We'll cover this in depth in Article 13: Observability — Reading Inngest's Run Logs (coming soon).
Inngest's Mental Model: Three Concepts
Inngest's entire API is built around three concepts. Understand these and everything else follows naturally.
Concept 1: Events
You already know what events are from Article 2. In Inngest, events are exactly that — named, structured records of things that happened.
You send an event to Inngest using inngest.send():
await inngest.send({
name: "user.signed_up",
data: {
userId: "usr_01J8G447",
email: "alex@example.com",
plan: "free_trial",
},
});
That's it. The event is now in Inngest's hands. It will be durably stored, matched to any registered functions, and those functions will be invoked.
Events in Inngest serve three purposes:
- Triggering functions — the most common use. An event fires and one or more functions run.
- Communicating between functions — a function can pause and wait for a specific event to arrive before continuing (we cover this in Article 8: Step Coordination — Waiting for External Events (coming soon)).
- Cancelling functions — you can cancel a running function by sending a matching event (useful for "user deleted their account — cancel all pending work").
Concept 2: Functions
An Inngest function is a background task registered in your codebase. It has three parts, described clearly in Inngest's GitHub repository:
Trigger — what causes the function to run. This is usually an event name, but can also be a cron schedule.
Flow control — optional configuration for how the function runs: concurrency limits, throttling, rate limiting, debouncing, prioritization, batching.
Handler — the actual code to run, expressed as an async function with access to the event data and a step object.
export const sendWelcomeEmail = inngest.createFunction(
// Part 1: Configuration + flow control
{
id: "send-welcome-email", // Unique identifier for this function
retries: 3, // Retry up to 3 times on failure
concurrency: {
limit: 10, // Run at most 10 instances at once
},
},
// Part 2: Trigger
{ event: "user.signed_up" }, // Run when this event fires
// Part 3: Handler
async ({ event, step }) => {
// event.data contains the payload from inngest.send()
await step.run("send-email", async () => {
await emailService.sendWelcome({
to: event.data.email,
name: event.data.name,
});
});
},
);
Functions live in your codebase, alongside your other application code. They're just TypeScript (or Python, or Go) — no special runtime, no special syntax beyond the Inngest SDK calls.
Concept 3: Steps
Steps are the heart of what makes Inngest different from a plain job queue.
A step is a named, checkpointed unit of work within a function. You define steps using step.run(). When a step completes successfully, its result is saved by Inngest. If the function fails later and retries, completed steps are skipped and their results are replayed — the function resumes from where it left off.
Think of steps like save points in a video game. You don't restart from the beginning every time you die — you restart from the last save point.
export const processOrder = inngest.createFunction(
{ id: "process-order" },
{ event: "order.placed" },
async ({ event, step }) => {
// Step 1: Charge the customer
// If this succeeds, the result is saved.
// If a later step fails, this step will NOT run again on retry.
const chargeResult = await step.run("charge-customer", async () => {
return await paymentService.charge({
customerId: event.data.customerId,
amount: event.data.totalAmount,
});
});
// Step 2: Update inventory
// Only runs after step 1 succeeds.
// Has access to chargeResult from the previous step.
await step.run("update-inventory", async () => {
await inventoryService.decrement(event.data.items);
});
// Step 3: Send confirmation email
// If this fails, steps 1 and 2 will NOT run again.
await step.run("send-confirmation", async () => {
await emailService.sendOrderConfirmation({
to: event.data.customerEmail,
orderId: event.data.orderId,
chargeId: chargeResult.id, // Using the saved result from step 1
});
});
},
);
You might wonder: "What if I don't wrap things in step.run()? What if I just write regular code in the function handler?" That works too — but that code won't be checkpointed. If the function fails and retries, any non-step code runs again from the beginning. Steps are how you opt into durability.
The step object also provides several other powerful primitives beyond step.run():
| Method | What it does |
|---|---|
step.run(id, fn) | Run a checkpointed unit of work |
step.sleep(id, duration) | Pause execution for a fixed duration |
step.sleepUntil(id, datetime) | Pause until a specific point in time |
step.waitForEvent(id, options) | Pause until a specific event arrives |
step.sendEvent(id, event) | Send an event from within a function |
step.invoke(id, function) | Invoke another Inngest function as a step |
We'll cover each of these in later articles. For now, just notice the range: steps aren't just for running code. They're for expressing workflow logic — waiting, sleeping, pausing, coordinating — in plain TypeScript.
How Inngest's Architecture Works
This is the part most tutorials skip — and it's the part that makes everything else make sense.
Here's the key insight: Inngest doesn't run your code. Your app runs your code. Inngest orchestrates when and how it runs.
Let's trace the full lifecycle of an Inngest function call:
┌──────────────────────────────────────────────────────────────────┐
│ Your App Inngest Platform │
│ │
│ 1. inngest.send({ │
│ name: "user.signed_up" ──────────────────────────────► │
│ }) 2. Stores event durably │
│ Matches to functions │
│ Schedules execution │
│ │
│ 4. Your handler runs ◄──────────────────────────────── │
│ step.run("send-email") 3. Calls your /api/inngest │
│ → runs the callback endpoint via HTTPS │
│ → returns result │
│ │
│ 5. Step result returned ──────────────────────────────► │
│ 6. Saves step result │
│ Schedules next step │
│ │
│ 7. Handler called again ◄──────────────────────────────── │
│ (next step) 8. Next step runs with │
│ step.run("setup-billing") previous results injected │
│ → runs the callback │
│ → returns result │
│ │
│ 9. Step result returned ──────────────────────────────► │
│ 10. Function complete ✅ │
└──────────────────────────────────────────────────────────────────┘
A few things to notice here:
Your function handler is called once per step. Inngest calls your /api/inngest endpoint to run each step. The function doesn't stay alive between steps. This is why it works on serverless platforms — each step is a fresh, short-lived function invocation.
State travels with each call. When Inngest calls your handler for step 2, it sends the saved results of step 1 along with the event payload. The SDK uses these to skip step 1 (via memoization) and jump straight to step 2.
Inngest is the orchestrator, your app is the executor. As Inngest's durable execution docs describe: Inngest is responsible for "invoking functions with the correct steps state (current step + previous steps data)" and "gathering each step result and scheduling the next step to perform." Your function's job is just to define the steps and run their logic.
This architecture is what enables Inngest to work on any platform — including serverless — without any long-running process. And it's what gives you durability without needing to manage external state storage yourself.
What the /api/inngest endpoint is
Your app needs to expose a single HTTP endpoint for Inngest to call. This is how Inngest invokes your functions. The Inngest SDK handles everything about this endpoint — you just register it once and forget about it.
In Next.js, it looks like this:
// app/api/inngest/route.ts
import { serve } from "inngest/next";
import { inngest } from "@/inngest/client";
import { sendWelcomeEmail, processOrder } from "@/inngest/functions";
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [sendWelcomeEmail, processOrder],
});
That's it. Inngest calls this endpoint with the event payload and step state. Your functions run. The SDK communicates back with the result. We'll cover this setup in detail in Article 4: Your First Inngest Function.
Inngest vs. Other Tools
Now that you understand what Inngest is, let's position it honestly against the alternatives. Knowing when not to use Inngest is just as important as knowing when to use it.
Inngest vs. BullMQ
BullMQ is a mature, powerful job queue for Node.js backed by Redis. It's battle-tested, has a large community, and is excellent for Node.js-only teams who already run Redis.
Choose BullMQ when:
- You're Node.js-only and already have Redis in your infrastructure
- You need very high throughput with low latency (sub-millisecond job enqueuing)
- You want full control over your queue infrastructure without any external dependency
Choose Inngest when:
- You need multi-step durable workflows, not just simple background jobs
- You're on serverless (Vercel, Netlify, Lambda) and can't run long-lived workers
- You want built-in observability without setting up Prometheus/Grafana
- You're working across multiple languages (TypeScript + Python, for example)
The fundamental difference: BullMQ is a queue. Inngest is a workflow engine. For simple "fire and forget" jobs, BullMQ might be simpler. For multi-step workflows with retries, waits, and coordination, Inngest has much better abstractions.
Inngest vs. Temporal
Temporal is a powerful durable execution engine that evolved from Uber's internal Cadence project. It's arguably the most feature-complete workflow platform available — and also the most complex to operate.
As Inngest's comparison page explains, one key difference is how code executes. Temporal "proxies" your code through its library, modifying the runtime to intercept function calls for state management. Inngest "uses native language primitives for direct execution" — your code runs as normal TypeScript/Python/Go, with no runtime modification.
Choose Temporal when:
- You need multi-language workflows orchestrated from a single workflow definition
- You require exactly-once execution semantics at enterprise scale
- Your team has the infrastructure expertise to run and maintain a Temporal cluster
Choose Inngest when:
- You want to get started in minutes without infrastructure setup
- Your team is primarily TypeScript/JavaScript or Python
- You're on serverless or want to stay on your existing hosting platform
- Developer experience and observability matter more than maximum flexibility
As a comparative review of workflow tools summarises: "JavaScript enthusiasts will appreciate the simplicity of Trigger.dev and the scalability of Inngest... those seeking complex, long-running workflows may find Temporal indispensable."
Inngest vs. AWS Step Functions
AWS Step Functions is Amazon's native workflow service, deeply integrated with the AWS ecosystem. It uses JSON/YAML to define workflow state machines.
Choose Step Functions when:
- You're all-in on AWS and need deep integration with Lambda, DynamoDB, SQS, etc.
- Your workflows are primarily orchestrating AWS services
- You need strict compliance with AWS's audit trail and IAM controls
Choose Inngest when:
- You're not AWS-only (Vercel, Railway, Render, self-hosted)
- You want to define workflows in code rather than JSON configuration
- You value a better local development experience (Step Functions requires mocking AWS services locally)
Quick comparison table
| Inngest | BullMQ | Temporal | Step Functions | |
|---|---|---|---|---|
| Infrastructure | Managed (zero-infra) | Redis required | Server cluster | AWS-only |
| Languages | TS, Python, Go | Node.js only | Many | AWS services |
| Serverless | ✅ First-class | ⚠️ Requires adapters | ⚠️ Limited | ✅ (Lambda) |
| Durable steps | ✅ Built-in | ❌ Manual | ✅ Built-in | ✅ Built-in |
| Observability | ✅ Built-in | ⚠️ External tools | ⚠️ External tools | ⚠️ CloudWatch |
| Local dev | ✅ Dev Server | ⚠️ Manual setup | ⚠️ Complex | ❌ AWS mocking |
| Learning curve | Low | Low | High | Medium |
| Best for | Modern apps, serverless | High-throughput Node.js | Enterprise/complex | AWS-native |
What Inngest Does NOT Do
Being precise about Inngest's limitations is just as important as understanding its strengths. Here's what you should know upfront:
Inngest is not a message broker. It doesn't support pub/sub between microservices in the Kafka sense. It's not designed for streaming high-throughput time-series data. For event streaming at Kafka scale, use Kafka.
Inngest is not a general-purpose scheduler. While Inngest supports cron-triggered functions, it's not a replacement for a dedicated scheduler like APScheduler or a cron server if you have many hundreds of distinct scheduled jobs.
Inngest functions have a dependency on the Inngest platform. If you're using Inngest Cloud (the managed service), your function execution depends on Inngest's uptime. For teams with strict availability requirements, Inngest also offers a self-hosted option — but that reintroduces infrastructure management.
Step results must be serialisable to JSON. Because step results are persisted externally, they must be JSON-serialisable. You can't return a database connection, a stream, or a class instance from a step.
Functions are not long-running processes. Your function handler is called once per step — it doesn't stay alive between steps. This is a feature (works on serverless) but also a constraint (you can't hold an open connection across steps).
A Complete Mental Model in One Picture
Let's crystallise everything into a single diagram before we move on:
┌─────────────────────────────┐
│ Your App │
│ │
User action │ API Route │
or external event ──────► │ await inngest.send({ │
│ name: "order.placed", │
│ data: { ... } │
│ }) │
│ ↓ │
│ return 200 OK ──────────► User
└──────────┬──────────────────┘
│ HTTPS
▼
┌─────────────────────────────┐
│ Inngest Platform │
│ │
│ ① Receive & store event │
│ ② Match to functions │
│ ③ Schedule execution │
└──────────┬──────────────────┘
│ HTTPS (calls your endpoint)
▼
┌─────────────────────────────┐
│ Your App │
│ │
│ /api/inngest endpoint │
│ │
│ processOrder function: │
│ ┌──────────────────────┐ │
│ │ step.run("charge") │ │
│ │ → result saved ✅ │ │
│ └──────────────────────┘ │
│ ┌──────────────────────┐ │
│ │ step.run("inventory")│ │
│ │ → result saved ✅ │ │
│ └──────────────────────┘ │
│ ┌──────────────────────┐ │
│ │ step.run("email") │ │
│ │ → result saved ✅ │ │
│ └──────────────────────┘ │
└─────────────────────────────┘
Your app does two things with Inngest: it sends events, and it serves a handler endpoint. Everything else — durability, retries, scheduling, observability, fan-out, state persistence — is Inngest's job.
Common Misconceptions
❌ Misconception: Inngest replaces your database
Reality: Inngest persists step results and event payloads for workflow execution purposes. It is not a general-purpose database. Your business data still lives in your own database. Inngest stores the execution state needed to resume functions after failure.
❌ Misconception: You need to rewrite your app to use Inngest
Reality: Inngest works alongside your existing codebase. You add the SDK, register the /api/inngest endpoint, and write functions. Your existing API routes, database calls, and business logic stay exactly where they are. Most teams adopt Inngest incrementally — one workflow at a time.
❌ Misconception: Steps are always better than plain code in a function
Reality: Not everything needs to be a step. Simple, fast, idempotent operations don't need the overhead of a checkpoint. Use step.run() when:
- The operation might fail and you'd want to retry it independently
- The operation has a side effect you don't want to repeat on retry (sending an email, charging a card)
- The result needs to be available to subsequent steps
Don't wrap a simple in-memory calculation in step.run() — it adds network round-trips to Inngest's state store unnecessarily.
❌ Misconception: Inngest is only for Next.js
Reality: Inngest works with any framework that can serve HTTP endpoints. Next.js, Express, Fastify, Hono, Remix, Nuxt, SvelteKit, plain Node.js, Python (FastAPI, Flask, Django), Go — if it can handle an HTTP request, it can host Inngest functions.
Check Your Understanding
Quick Quiz
1. What is the key difference between Inngest and BullMQ?
Show Answer
BullMQ is a job queue — it handles enqueueing jobs and running them in worker processes backed by Redis. Inngest is a workflow engine with durable execution — it handles multi-step functions where each step is checkpointed, so failed functions resume from where they left off rather than restarting. BullMQ requires a long-running Node.js process. Inngest works on serverless because each step is a separate, short-lived HTTP call to your endpoint.
2. A function has three steps. Steps 1 and 2 complete. Step 3 fails and exhausts its retries. What happens to the data from steps 1 and 2?
Show Answer
The results from steps 1 and 2 are saved in Inngest's state store. They are not lost. When you fix the bug and manually replay the function run from the Inngest dashboard, the function resumes from step 3 — steps 1 and 2 are skipped and their saved results are replayed automatically. This is the core value of durable execution.
3. Why does Inngest call your /api/inngest endpoint instead of you polling Inngest for work?
Show Answer
This push model (Inngest calls you) rather than pull model (you poll Inngest) is what enables Inngest to work on serverless platforms. A pull model requires your app to have a long-running process constantly checking for new work — impossible on platforms like Vercel that shut down idle functions. With the push model, your function only runs when Inngest calls it, making it compatible with ephemeral serverless environments.
Summary: Key Takeaways
- Inngest is a durable execution platform — it combines a queue, a workflow engine, and observability into a managed service you connect over HTTP.
- The three core concepts are events (what happened), functions (what to do about it), and steps (checkpointed units of work inside a function).
- Durable execution means steps are checkpointed — on failure and retry, completed steps are skipped and their results replayed. No duplicate side effects.
- Inngest orchestrates, your app executes. Inngest calls your
/api/inngestendpoint once per step. Your function doesn't need to stay alive between steps. - It works on serverless because the push-based model doesn't require a long-running process.
- Use Inngest over BullMQ when you need multi-step workflows with durability. Use Temporal for enterprise-scale mission-critical workloads. Use Step Functions if you're AWS-native. Use Inngest when you want developer experience and operational simplicity.
- Inngest is not a message broker, not a database, and not a general-purpose scheduler. Know its scope.
What's Next?
You now understand what Inngest is, why it exists, and how it fits into the wider landscape. The mental model — events trigger functions, functions are composed of steps, steps are durable — is in place.
In Article 4: Your First Inngest Function, we put all of this into practice. You'll install the SDK, register the /api/inngest endpoint, write your first function, send your first event, and watch it all run in the Dev Server — step by step, from zero.
Version Information
Tested with:
- Inngest SDK (TypeScript):
inngest@^3.x - Node.js: v18.x, v20.x, v22.x
- TypeScript: 5.x
Further reading:
- Inngest Documentation — official SDK reference and guides
- The Principles of Durable Execution — deep dive into how step checkpointing works
- How Inngest Functions Are Executed — the full technical walkthrough of the execution model
- Inngest vs Temporal — official comparison with code examples
- Simplify Your Queues with Inngest — positioning against Kafka and traditional queues