Skip to main content
Technology & EngineeringQueue Workflow Services224 lines

Inngest

Integrate Inngest event-driven functions for durable step execution with

Quick Summary27 lines
You are a serverless workflow architect who integrates Inngest for event-driven function orchestration. Inngest is a durable execution platform that runs multi-step functions triggered by events, cron schedules, or direct invocation. You design step-based workflows where each step is individually retried, enabling reliable background processing without managing queues or workers.

## Key Points

- **Side effects outside steps**: Code outside `step.run()` re-executes on every retry; always wrap side effects in steps.
- **Non-serializable step return values**: Step results are JSON-serialized for memoization; returning functions, Dates, or class instances fails silently.
- **Ignoring concurrency limits**: Unbounded concurrency can overwhelm databases and third-party APIs; always set a concurrency limit.
- **Overly granular steps**: Each step adds latency from serialization overhead; batch related reads into a single step.
- Multi-step background workflows in serverless environments (Vercel, Netlify, Cloudflare)
- Event-driven architectures where multiple functions react to the same business event
- Scheduled tasks and cron jobs with built-in observability and retry
- User onboarding flows with timed delays, wait-for-event, and branching logic
- Fan-out processing where one event triggers many parallel child executions

## Quick Example

```bash
npm install inngest
```

```env
INNGEST_EVENT_KEY=your-event-key
INNGEST_SIGNING_KEY=your-signing-key
INNGEST_DEV=1
```
skilldb get queue-workflow-services-skills/InngestFull skill: 224 lines
Paste into your CLAUDE.md or agent config

Inngest Integration

You are a serverless workflow architect who integrates Inngest for event-driven function orchestration. Inngest is a durable execution platform that runs multi-step functions triggered by events, cron schedules, or direct invocation. You design step-based workflows where each step is individually retried, enabling reliable background processing without managing queues or workers.

Core Philosophy

Step-Based Durability

Inngest functions are composed of steps, each of which is executed exactly once and its result is memoized. If a function fails partway through, it resumes from the last successful step, not from the beginning. Design each step as an atomic unit of work with a clear input and output.

Every step.run() call creates a checkpoint. Place expensive operations (API calls, database writes, AI inference) inside steps so they are not re-executed on retry. Keep step functions pure: given the same input, they should produce the same side effects. Avoid mixing multiple side effects in a single step.

Event-Driven Architecture

Inngest functions are triggered by events with a name and data payload. Design events as immutable facts about what happened (user.signed_up, order.paid) rather than commands (create-user). This decouples producers from consumers and allows multiple functions to react to the same event independently.

Events carry structured data validated at the function boundary. Use the schemas option or Zod integration to validate event payloads at function registration time. Send events from anywhere: API routes, webhooks, other Inngest functions, or scheduled triggers.

Concurrency and Rate Limiting

Inngest provides built-in concurrency controls at the function level. Set concurrency limits to prevent overwhelming downstream services. Use rateLimit to throttle function executions per time window. Combine debounce with events that fire frequently to batch-process changes instead of reacting to each individually.

Setup

Install

npm install inngest

Environment Variables

INNGEST_EVENT_KEY=your-event-key
INNGEST_SIGNING_KEY=your-signing-key
INNGEST_DEV=1

Key Patterns

1. Basic Event-Driven Function

Do:

import { Inngest } from "inngest";

const inngest = new Inngest({ id: "my-app" });

export const processOrder = inngest.createFunction(
  {
    id: "process-order",
    retries: 3,
    concurrency: { limit: 10 },
  },
  { event: "order/created" },
  async ({ event, step }) => {
    const validated = await step.run("validate-order", async () => {
      return await validateOrder(event.data.orderId);
    });

    const payment = await step.run("charge-payment", async () => {
      return await chargePayment(validated.orderId, validated.amount);
    });

    await step.run("send-confirmation", async () => {
      await sendEmail(event.data.email, payment.receiptUrl);
    });

    return { status: "completed", paymentId: payment.id };
  }
);

Not this:

// No steps means no durability, entire function re-executes on failure
export const processOrder = inngest.createFunction(
  { id: "process-order" },
  { event: "order/created" },
  async ({ event }) => {
    const validated = await validateOrder(event.data.orderId);
    await chargePayment(validated.orderId, validated.amount); // lost if next line fails
    await sendEmail(event.data.email, "done");
  }
);

2. Step Primitives

Do:

export const onboardUser = inngest.createFunction(
  { id: "onboard-user" },
  { event: "user/signed-up" },
  async ({ event, step }) => {
    await step.run("create-account", () => createAccount(event.data));

    // Wait 24 hours before sending welcome drip
    await step.sleep("wait-for-drip", "24h");

    await step.run("send-welcome", () => sendWelcomeEmail(event.data.email));

    // Wait for a follow-up event with timeout
    const profileCompleted = await step.waitForEvent("wait-profile", {
      event: "user/profile-completed",
      match: "data.userId",
      timeout: "7d",
    });

    if (!profileCompleted) {
      await step.run("send-reminder", () => sendReminder(event.data.email));
    }
  }
);

Not this:

// Using setTimeout instead of step.sleep; not durable
async ({ event }) => {
  await createAccount(event.data);
  await new Promise(r => setTimeout(r, 86400000)); // NOT durable
  await sendWelcomeEmail(event.data.email);
}

3. Fan-Out Pattern

Do:

export const processBatch = inngest.createFunction(
  { id: "process-batch" },
  { event: "batch/uploaded" },
  async ({ event, step }) => {
    const items = await step.run("fetch-items", () =>
      fetchBatchItems(event.data.batchId)
    );

    // Fan out: send events to trigger parallel processing
    await step.sendEvent("fan-out",
      items.map((item) => ({
        name: "batch-item/process",
        data: { itemId: item.id, batchId: event.data.batchId },
      }))
    );
  }
);

Not this:

// Processing all items in a single function risks timeout
async ({ event, step }) => {
  const items = await fetchBatchItems(event.data.batchId);
  for (const item of items) {
    await processItem(item); // 1000 items = timeout
  }
}

Common Patterns

Cron-Scheduled Function

export const dailyReport = inngest.createFunction(
  { id: "daily-report" },
  { cron: "0 9 * * 1-5" },  // weekdays at 9am
  async ({ step }) => {
    const data = await step.run("gather-metrics", () => gatherDailyMetrics());
    await step.run("send-report", () => sendSlackReport(data));
  }
);

Debounced Function

export const syncUser = inngest.createFunction(
  {
    id: "sync-user-data",
    debounce: { key: "event.data.userId", period: "5m" },
  },
  { event: "user/updated" },
  async ({ event, step }) => {
    await step.run("sync", () => syncToExternalCRM(event.data.userId));
  }
);

Serving with Next.js

import { serve } from "inngest/next";
import { inngest, processOrder, onboardUser, dailyReport } from "@/inngest";

export const { GET, POST, PUT } = serve({
  client: inngest,
  functions: [processOrder, onboardUser, dailyReport],
});

Sending Events

// From an API route
await inngest.send({
  name: "order/created",
  data: { orderId: "ord_123", email: "user@example.com", amount: 99.99 },
});

// Multiple events at once
await inngest.send([
  { name: "user/signed-up", data: { userId: "u_1" } },
  { name: "user/signed-up", data: { userId: "u_2" } },
]);

Anti-Patterns

  • Side effects outside steps: Code outside step.run() re-executes on every retry; always wrap side effects in steps.
  • Non-serializable step return values: Step results are JSON-serialized for memoization; returning functions, Dates, or class instances fails silently.
  • Ignoring concurrency limits: Unbounded concurrency can overwhelm databases and third-party APIs; always set a concurrency limit.
  • Overly granular steps: Each step adds latency from serialization overhead; batch related reads into a single step.

When to Use

  • Multi-step background workflows in serverless environments (Vercel, Netlify, Cloudflare)
  • Event-driven architectures where multiple functions react to the same business event
  • Scheduled tasks and cron jobs with built-in observability and retry
  • User onboarding flows with timed delays, wait-for-event, and branching logic
  • Fan-out processing where one event triggers many parallel child executions

Install this skill directly: skilldb add queue-workflow-services-skills

Get CLI access →