How to Use Inngest: The Beginner's Guide (First Function, Events, step.run, and the Dev Server)
CodexFuse Team · 2026-11-01T15:59:07.665Z · 16 min read min read
Learning how to use Inngest takes about an hour, and by the end you will have a backend function that no timeout can kill. Inngest is a durable-execution platform for JavaScript and TypeScript developers: you write ordinary functions, wrap each piece of work in step.run(), and if the process crashes mid-way, the function resumes from the last checkpoint instead of starting over. This inngest tutorial runs the complete beginner arc in one place: the mental-model lesson, installing the SDK, creating the Inngest client, the event lesson, your first function, the serve route that exposes it, sending events, the dev-server habit for local testing, flow control, retries, scheduled functions, and deploying to production. If you have never containerized a dev environment, start with our Docker beginner guide — the local pieces below run happily inside a container. One naming note before anything else: this guide is about the Inngest durable-execution platform, not the Indian medicine brand of the same name, and not Elasticsearch's "ingest node" — those are the homonyms polluting the search results for this exact keyword.
What Is Inngest?
Inngest is an open-source SDK for orchestrating event-driven apps and agents — that is the company's own one-line description, and it is accurate. In practical terms, it is the reliability layer for your backend: you define functions that are triggered by events or cron schedules, and every step inside a function is checkpointed, so crashes, deploys, and timeouts resume where they stopped instead of re-running everything.
Three words do all the work here. Events go in — named JSON payloads that trigger your functions. Steps are the checkpointed units of work inside a function. Functions are the durable units that run them. Once you can say "an event triggers a function, and each step.run is a checkpoint," you understand Inngest.
Why Do Developers Use Inngest? The Timeout Problem
Because serverless functions have a hard ceiling, and real work does not care about it. A payment flow that charges, waits three days for a webhook, then ships the order cannot survive inside a function that dies after a few minutes — the classic Next.js background jobs problem.
Vercel's current limits make this concrete: with Fluid Compute (the default for new projects), every plan defaults to 300 seconds of function duration, and the Hobby plan caps at 300 seconds. Past the limit, the invocation dies with a 504 FUNCTION_INVOCATION_TIMEOUT and nothing retries it. Inngest sidesteps this completely — step.sleep() can pause a function for days, and the free plan allows a single run to last up to 30 days. The code sleeps; the platform remembers.
Step 1: Install the SDK and Create the Client
Installation is one command: npm install inngest — the JavaScript/TypeScript SDK (the SDK source lives in the inngest/inngest-js repository). Every code sample in this guide follows the current TypeScript SDK v4 (GA March 2026), verified against the official docs and GitHub README at write time — older v1/v3 snippets use different shapes, so ignore anything that doesn't match what you see below.
Then create one Inngest client per app — that is the client lesson, said once. The client's id names your app, and every function you register attaches to it:
npm install inngest
// inngest/client.ts
import { Inngest } from "inngest";
export const inngest = new Inngest({ id: "my-app" });
Keep your code in a repository from the start — our GitHub beginner guide covers the repo habit if you need it. One client, one id, imported everywhere you define functions or send events.
Step 2: The Event Lesson — Events Are the Trigger Contract
Events are named JSON payloads, and they are the trigger contract — name them carefully, because they are the API your whole system speaks. Every event has a name (a string, by convention a dotted or slashed path like shop/order.created) and a data object (any JSON-serializable payload).
The discipline matters more than the syntax. Rename an event later and every function listening for the old name goes silent — there is no compiler to catch it. Pick a naming scheme on day one (app/entity.action is the common pattern), write it down, and treat event names like public API endpoints.
Step 3: Write Your First Function — createFunction and step.run
createFunction takes three arguments: a config object, a trigger, and the handler. The config carries a stable id (required — Inngest uses it to track your function across deploys), an optional name, and options like retries. The trigger is either an event or a cron schedule. The handler receives { event, step }:
// inngest/functions.ts
import { inngest } from "./client";
export const importProductImages = inngest.createFunction(
{ id: "import-product-images", retries: 3 },
{ event: "shop/product.imported" },
async ({ event, step }) => {
const urls = await step.run("fetch-image-urls", async () => {
// returns JSON-serializable data, checkpointed here
return getImageUrls(event.data.productId);
});
await step.run("copy-images-to-s3", async () => {
await copyToS3(urls);
});
return { imported: urls.length };
}
);
Here is the Inngest step.run checkpoint lesson, said once: each step.run() is a checkpoint. If the process crashes during copy-images-to-s3, the function re-runs but skips fetch-image-urls — completed steps are never re-run. This is the whole reason Inngest exists: long, multi-step work that survives anything.
Step 4: Expose the Serve Route
The serve route is the HTTP endpoint Inngest calls to discover and execute the functions you register — said once. For Next.js App Router (the NextJS framework's app directory), it lives at src/app/api/inngest/route.ts and uses the inngest/next subpath of the SDK (no separate install):
// src/app/api/inngest/route.ts
import { serve } from "inngest/next";
import { inngest } from "@/inngest/client";
import { importProductImages } from "@/inngest/functions";
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [importProductImages],
});
The gotcha beginners hit: a function only exists if it appears in the functions array. Defining createFunction in a file is not enough — register every function in the serve call, or Inngest never sees it.
Step 5: Send Your First Event
Inngest events are sent with inngest.send() from anywhere in your app — an API route, a webhook handler, a button click:
await inngest.send({
name: "shop/product.imported",
data: { productId: "prod_123", imageURLs: ["https://..."] },
});
That single call triggers every function listening for shop/product.imported. Notice what the sender does not do: it does not call the function, wait for it, or know it exists. Events decouple the "something happened" from the "here is what we do about it" — that decoupling is the point.
The 9-Step Inngest Quickstart
If you want the whole arc before the detail, this is the complete beginner path from zero to a running durable function:
- Install the SDK:
npm install inngest(current TS SDK v4, verified at write time). - Create one client per app:
new Inngest({ id: "my-app" }). - Design your events: named JSON payloads,
app/entity.actionconvention, treated as a public API. - Write the first function:
createFunction({ id, retries }, { event }, handler)withstep.run()checkpoints. - Expose the serve route at
/api/inngestand register every function in thefunctionsarray. - Start the dev server:
npx inngest-cli@latest dev— no keys, no account needed. - Trigger a test event from the dev server's Invoke button and watch the run execute step by step.
- Send a real event with
inngest.send()from your app. - Deploy: event key in environment variables, sync the app so Inngest registers the production functions.
Step 6: The Dev-Server Habit — Test Everything Locally
Start the local dev server with npx inngest-cli@latest dev and open http://localhost:8288. It shows your Apps, Inngest functions, and Runs, and its Invoke button fires a test event payload at any function so you can watch each step execute, fail, and retry — without touching production.
Two facts make the dev-server habit stick. First, the Inngest dev server needs no keys and no account — you can do this entire exercise with zero signup. Second, point the v4 SDK at it with INNGEST_DEV=1. Production is where keys enter the picture: the event key and signing key live in environment variables, never in code — said once.
Flow Control: Sleep, Wait for Events, Invoke
Durable functions can wait, and waiting is cheap because nothing stays running. step.sleep("wait-1-hour", "1h") pauses the function without holding a request open — duration strings follow the familiar ms-package style ("1s", "30m", "2 days"). There is also step.sleepUntil(date) for pausing until a specific time.
step.waitForEvent pauses until a matching event arrives or a timeout hits — the classic approval-flow primitive:
const approval = await step.waitForEvent("wait-for-approval", {
event: "app/invoice.approved",
timeout: "7d",
match: "data.invoiceId",
});
If the approval event arrives within seven days, the function continues with its payload; if not, approval is undefined and your code takes the timeout path. For orchestration between functions, step.invoke() calls another function and waits for its result, and step.sendEvent() fires events from inside a function.
Retries and Error Handling
Retries are on by default: four retries after the initial attempt, per the official error-handling guide for the current SDK. Set retries: N in the function config (0–20; retries: 0 disables retries entirely). Each step.run() gets its own retry counter, and backoff uses jitter by default so a fleet of failing functions doesn't thundering-herd your database.
Two error classes give you control. Throw RetryAfterError when you hit a rate limit and want the step retried after a delay; throw NonRetriableError for permanent failures like invalid input, where retrying would just burn executions. The common beginner mistake is letting a validation error retry four times — classify your errors.
Scheduled Functions With Cron
Swap the event trigger for a cron trigger and the function runs on a schedule — no event payload involved:
export const dailyReport = inngest.createFunction(
{ id: "daily-report" },
{ cron: "0 9 * * *" }, // every day at 9:00 AM, unix-cron format
async ({ step }) => {
await step.run("build-report", async () => buildReport());
await step.run("send-report", async () => sendReport());
}
);
Prefix the schedule with a timezone when it matters (TZ=Europe/Paris 0 12 * * 5), and note that scheduled functions receive no event argument — there is no triggering payload, just the clock.
Deploying to Production
Production has two lessons. First, never hardcode keys: the event key (INNGEST_EVENT_KEY) and signing key live in environment variables — the dev server needed none, production requires them. Second, sync your app after every deploy: Inngest discovers functions through the serve route, so a deploy that adds or changes a function must re-sync before the new definitions take effect.
You have two connection options. serve() exposes the HTTP endpoint and Inngest calls it; connect() runs a long-lived worker that polls Inngest instead. Either way your code can live anywhere — Vercel, AWS, Cloudflare, your own VPS (read what VPS hosting actually is before renting one). Self-hosting the Inngest server itself is also supported, so the orchestration layer can stay on your infrastructure too.
Inngest vs Trigger.dev vs Temporal vs Pipedream
The durable-execution field has four names beginners mix up. Here is the one-table version:
| Inngest | Trigger.dev | Temporal | Pipedream | |
|---|---|---|---|---|
| What it is | Zero-infra SDK: durable step functions inside your own codebase | Background-jobs platform that also bundles the compute running your code | Infrastructure-grade workflow engine for complex long-running orchestration | Hosted code-first automation platform for API workflows |
| Where your code runs | Your own app/hosting (via serve or connect) | Trigger.dev's managed compute | Your workers + the Temporal cluster | Pipedream's hosted platform |
| Self-hostable | Yes — the Inngest server can self-host | Limited | Yes | No |
| Pick it when | You want durable functions in the app you already have, with zero new infrastructure | You want jobs + compute from one vendor | You need heavy multi-week orchestration with an explicit event history | You want hosted API automation without managing anything |
The draw-the-line version: Trigger.dev is the direct competitor in the durable background-jobs lane, but it bundles compute while Inngest only orchestrates your own code over HTTP. Temporal is a level up in operational weight — right for complex orchestration, overkill for "my function keeps timing out." Pipedream is a different lane (hosted automation), and worth knowing that its Workflows product shuts down March 31, 2027 — its lane is moving, Inngest's is not.
Inngest Pricing
The free tier is genuinely usable: $0 for 50,000 executions a month (one execution = a single durable function run or step execution), 5 concurrent steps, a 30-day maximum run length, 5 seats, and 3 workers — no credit card required. That covers most side projects and early products entirely.
Paid tiers scale on executions: $99/month includes 1 million executions (then $50 per million), $499/month includes 10 million (then $25 per million), with higher concurrent-step limits and longer maximum runs. Sandboxes — isolated microVMs inside functions, currently in public beta — are billed per second of vCPU and memory. As always, verify the numbers on the official pricing page before budgeting; they move.
7 Beginner Mistakes That Break an Inngest Setup
| Mistake | Why it hurts | The fix |
|---|---|---|
| Naming events carelessly | Events are the trigger contract — a rename silently disconnects every listener | Pick the app/entity.action convention on day one and treat names as public API |
| Hardcoding the event key | Keys leak into git history and screenshots | INNGEST_EVENT_KEY in environment variables, always |
| Forgetting to register the function | Defined but not in the serve functions array = invisible to Inngest | Register every function in serve(); re-check after adding new files |
| Putting side effects outside step.run | Un-checkpointed work re-runs on every retry | Everything with side effects goes inside a step.run() |
| Using plain sleep instead of step.sleep | Keeps the request open and hits the timeout you were escaping | step.sleep() — the pause that costs nothing while waiting |
| Skipping the dev server | First-run bugs discovered in production, with production retries | npx inngest-cli@latest dev + the Invoke button before every deploy |
| Expecting an event in a cron function | Scheduled functions receive no event argument — event is undefined | Read inputs from config or a database, not from the trigger |
Sources
Every fact in this guide was verified against a page fetched on November 1, 2026: the Inngest TypeScript quickstart docs (install, client, serve route, dev server, INNGEST_DEV), the Inngest docs on durable-execution primitives (step.run, step.sleep, step.waitForEvent, step.invoke), the Inngest error-handling guide (default 4 retries, backoff, RetryAfterError, NonRetriableError), the Inngest deploying-functions guide (serve vs connect), the official Inngest pricing page (free tier 50,000 executions, $99 and $499 plans, sandbox billing), the Inngest changelog (TypeScript SDK v4 GA March 13, 2026; sandboxes public beta), the Inngest GitHub repository (6,008 stars, SSPL server license, Apache-2.0 SDKs, dev server on localhost:8288), the JSR @inngest/sdk reference (createFunction signature), the aiworkshack Inngest quickstart (serve route shape, dev command), the appstackbuilder Inngest-vs-Trigger.dev-vs-Temporal 2026 comparison, the Hookdeck Pipedream-alternatives page (Workflows shutdown March 31, 2027), and the Hyperping and Monogram pieces on Vercel Fluid Compute function limits (checked against Vercel's docs).
Frequently Asked Questions
Is Inngest free to use?
Yes, for most beginners. The free plan costs $0, needs no credit card, and includes 50,000 executions a month with a 30-day maximum run length — enough for side projects and early products. You pay only when your execution volume outgrows it.
Does Inngest run my code?
No — and that is the key difference from Trigger.dev, which bundles the compute. Your functions run inside your own app and hosting; Inngest orchestrates them over HTTP via the serve route (or a long-lived worker with connect). You keep your infrastructure; Inngest adds durability, steps, and retries.
What happens if my function crashes halfway through?
It resumes from the last completed checkpoint. Each step.run() is a checkpoint: completed steps are never re-run, and the function picks up at the first incomplete step. Crashes, deploys, and timeouts all recover the same way.
Can I use Inngest without Next.js?
Yes. The platform ships SDKs for Python, Go, and Kotlin alongside the TypeScript SDK, with serve adapters for other frameworks, and the Inngest server itself can be self-hosted. Next.js is the best-documented path, not the only one.
How is Inngest different from a cron job or a queue?
A cron job fires on a clock with no memory of past runs; a queue moves messages without checkpointing multi-step state. Inngest is durable execution: event triggers plus cron triggers, steps with checkpoints, retries with backoff, sleeps measured in days, and waits for events — the whole state machine, not just the trigger.