Inngest
"Inngest: event-driven functions, durable workflows, step functions, retries, cron, fan-out, sleep, Next.js integration"
Inngest provides durable, event-driven functions that run reliably without infrastructure management. Functions are triggered by events, executed as step functions with automatic retries, and orchestrated through a declarative SDK. The core idea is that background work should be as simple as writing a function — Inngest handles queuing, retries, concurrency, observability, and state management. You ship functions, not infrastructure. ## Key Points - **Keep steps small and idempotent.** Each step can retry independently; side effects should be safe to repeat. - **Use step.sleep for delays** instead of setTimeout. Durable sleep costs nothing and survives restarts. - **Type your events** with EventSchemas to get compile-time safety across your entire event-driven system. - **Use waitForEvent** for human-in-the-loop or external system coordination instead of polling. - **Set concurrency limits** to protect downstream services from being overwhelmed. - **Return serializable data from steps.** Step outputs are persisted as JSON for checkpointing. - **Use the Inngest dev server** during development for local testing with full visibility into function execution. - **Register all functions** in a single serve() endpoint so Inngest can discover them via a single sync. - **Giant monolithic steps.** A single step with 10 API calls means all 10 retry together on failure. Split into individual steps. - **Non-idempotent steps.** If a step sends an email and retries, the user gets duplicate emails. Use idempotency keys or check state before acting. - **Storing secrets in event payloads.** Events are logged and visible in the dashboard. Pass IDs and look up sensitive data inside steps. - **Ignoring step return values.** Steps must return serializable data. Returning database connection objects or class instances will break checkpointing. ## Quick Example ```typescript // .env.local INNGEST_EVENT_KEY=your-event-key // Production event key INNGEST_SIGNING_KEY=your-signing-key // Verifies requests from Inngest // Dev server runs automatically with `npx inngest-cli@latest dev` ```
skilldb get background-jobs-services-skills/InngestFull skill: 240 linesInngest
Core Philosophy
Inngest provides durable, event-driven functions that run reliably without infrastructure management. Functions are triggered by events, executed as step functions with automatic retries, and orchestrated through a declarative SDK. The core idea is that background work should be as simple as writing a function — Inngest handles queuing, retries, concurrency, observability, and state management. You ship functions, not infrastructure.
Inngest functions are durable: each step is checkpointed, so if a step fails, execution resumes from the last successful step rather than restarting from scratch. This makes complex multi-step workflows reliable by default.
Setup
Installation and Configuration
// Install dependencies
// npm install inngest
// lib/inngest/client.ts
import { Inngest } from "inngest";
export const inngest = new Inngest({
id: "my-app",
// Optional: event schemas for type safety
schemas: new EventSchemas().fromRecord<{
"user/signup": { data: { userId: string; email: string } };
"order/placed": { data: { orderId: string; total: number } };
"email/send": { data: { to: string; templateId: string; vars: Record<string, string> } };
}>(),
});
Next.js API Route Handler
// app/api/inngest/route.ts
import { serve } from "inngest/next";
import { inngest } from "@/lib/inngest/client";
import { onUserSignup } from "@/lib/inngest/functions/on-user-signup";
import { processOrder } from "@/lib/inngest/functions/process-order";
import { dailyReport } from "@/lib/inngest/functions/daily-report";
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [onUserSignup, processOrder, dailyReport],
});
Environment Configuration
// .env.local
INNGEST_EVENT_KEY=your-event-key // Production event key
INNGEST_SIGNING_KEY=your-signing-key // Verifies requests from Inngest
// Dev server runs automatically with `npx inngest-cli@latest dev`
Key Techniques
Step Functions with Checkpointing
// lib/inngest/functions/on-user-signup.ts
import { inngest } from "../client";
export const onUserSignup = inngest.createFunction(
{
id: "on-user-signup",
retries: 3,
concurrency: { limit: 10 },
},
{ event: "user/signup" },
async ({ event, step }) => {
// Each step is individually retried and checkpointed
const user = await step.run("fetch-user", async () => {
const res = await db.user.findUnique({ where: { id: event.data.userId } });
if (!res) throw new Error("User not found");
return res;
});
// This step runs only after fetch-user succeeds
await step.run("send-welcome-email", async () => {
await emailService.send({
to: user.email,
template: "welcome",
vars: { name: user.name },
});
});
// Sleep pauses execution durably — no compute used while waiting
await step.sleep("wait-before-onboarding", "3 days");
await step.run("send-onboarding-tips", async () => {
await emailService.send({
to: user.email,
template: "onboarding-tips",
});
});
return { success: true, userId: user.id };
}
);
Cron-Triggered Functions
// lib/inngest/functions/daily-report.ts
import { inngest } from "../client";
export const dailyReport = inngest.createFunction(
{ id: "daily-report" },
{ cron: "0 9 * * *" }, // Every day at 9 AM UTC
async ({ step }) => {
const stats = await step.run("gather-stats", async () => {
const [users, orders, revenue] = await Promise.all([
db.user.count({ where: { createdAt: { gte: yesterday() } } }),
db.order.count({ where: { createdAt: { gte: yesterday() } } }),
db.order.aggregate({ _sum: { total: true }, where: { createdAt: { gte: yesterday() } } }),
]);
return { users, orders, revenue: revenue._sum.total ?? 0 };
});
await step.run("send-report", async () => {
await slack.postMessage({
channel: "#daily-metrics",
text: `Daily: ${stats.users} signups, ${stats.orders} orders, $${stats.revenue} revenue`,
});
});
}
);
Fan-Out Pattern
// lib/inngest/functions/process-order.ts
import { inngest } from "../client";
export const processOrder = inngest.createFunction(
{ id: "process-order", retries: 5 },
{ event: "order/placed" },
async ({ event, step }) => {
const order = await step.run("load-order", async () => {
return db.order.findUniqueOrThrow({
where: { id: event.data.orderId },
include: { items: true, customer: true },
});
});
// Fan-out: send multiple events to trigger parallel downstream functions
await step.sendEvent("notify-systems", [
{ name: "email/send", data: { to: order.customer.email, templateId: "order-confirm", vars: { orderId: order.id } } },
{ name: "inventory/reserve", data: { items: order.items } },
{ name: "analytics/track", data: { event: "purchase", value: order.total } },
]);
// Wait for external webhook callback using step.waitForEvent
const shippingUpdate = await step.waitForEvent("wait-for-shipment", {
event: "shipping/label-created",
match: "data.orderId",
timeout: "7d",
});
if (shippingUpdate) {
await step.run("notify-shipped", async () => {
await emailService.send({
to: order.customer.email,
template: "order-shipped",
vars: { trackingNumber: shippingUpdate.data.trackingNumber },
});
});
}
}
);
Sending Events from Application Code
// app/api/orders/route.ts
import { inngest } from "@/lib/inngest/client";
export async function POST(req: Request) {
const body = await req.json();
const order = await db.order.create({ data: body });
// Fire-and-forget: Inngest handles the rest
await inngest.send({
name: "order/placed",
data: { orderId: order.id, total: order.total },
});
return Response.json({ orderId: order.id });
}
Throttling and Rate Limiting
export const rateLimitedSync = inngest.createFunction(
{
id: "sync-to-crm",
throttle: { limit: 100, period: "1m" }, // Max 100 invocations per minute
retries: 3,
concurrency: { limit: 5 }, // Max 5 running at once
},
{ event: "contact/updated" },
async ({ event, step }) => {
await step.run("sync", async () => {
await crmClient.upsertContact(event.data.contactId);
});
}
);
Best Practices
- Keep steps small and idempotent. Each step can retry independently; side effects should be safe to repeat.
- Use step.sleep for delays instead of setTimeout. Durable sleep costs nothing and survives restarts.
- Type your events with EventSchemas to get compile-time safety across your entire event-driven system.
- Use waitForEvent for human-in-the-loop or external system coordination instead of polling.
- Set concurrency limits to protect downstream services from being overwhelmed.
- Return serializable data from steps. Step outputs are persisted as JSON for checkpointing.
- Use the Inngest dev server during development for local testing with full visibility into function execution.
- Register all functions in a single serve() endpoint so Inngest can discover them via a single sync.
Anti-Patterns
- Giant monolithic steps. A single step with 10 API calls means all 10 retry together on failure. Split into individual steps.
- Non-idempotent steps. If a step sends an email and retries, the user gets duplicate emails. Use idempotency keys or check state before acting.
- Storing secrets in event payloads. Events are logged and visible in the dashboard. Pass IDs and look up sensitive data inside steps.
- Ignoring step return values. Steps must return serializable data. Returning database connection objects or class instances will break checkpointing.
- Using try/catch to swallow errors in steps. Let errors propagate so Inngest can retry. Handle expected failures with conditional logic, not silent catches.
- Polling in a loop inside a function. Use waitForEvent or step.sleep with retry logic instead of busy-waiting inside a step.
Install this skill directly: skilldb add background-jobs-services-skills
Related Skills
BullMQ
"BullMQ: Redis-based job queue, workers, delayed jobs, rate limiting, job priorities, repeatable jobs, concurrency, dashboard"
Faktory
"Faktory: polyglot background job server, language-agnostic workers, job priorities, retries, scheduled jobs, batches, middleware, Web UI"
Graphile Worker
"Graphile Worker: PostgreSQL-backed job queue, no Redis needed, cron jobs, batch jobs, task isolation, migrations, LISTEN/NOTIFY, Node.js"
Quirrel
"Quirrel: job queue for serverless/edge, cron jobs, delayed jobs, repeat scheduling, Next.js/Remix/SvelteKit integration, type-safe queues"
Temporal
"Temporal: durable execution, workflows, activities, signals, queries, retries, timers, TypeScript SDK"
Trigger.dev
"Trigger.dev: background jobs for Next.js/Node, long-running tasks, integrations, retry, cron, dashboard, v3 SDK"