Inngest: Event-Driven TypeScript Functions Without Infra
HOW TO GUIDES April 14, 2026, 11:30 a.m.

Inngest: Event-Driven TypeScript Functions Without Infra

Inngest is a fresh take on serverless that lets you write event‑driven functions in TypeScript without wrestling with the underlying infrastructure. Think of it as a blend of webhooks, background jobs, and durable functions—all wrapped in a developer‑friendly SDK. In this article we’ll walk through the core concepts, set up a project from scratch, and build a couple of real‑world examples that showcase Inngest’s power. By the end, you’ll be ready to drop event‑driven logic into any Node.js or Next.js app with confidence.

What Makes Inngest Different?

Traditional serverless platforms (AWS Lambda, Vercel Functions, Cloudflare Workers) give you a way to run code on demand, but they often leave you to manage queues, retries, and state manually. Inngest abstracts those operational concerns into a single API that records every event, guarantees at‑least‑once delivery, and provides a built‑in UI for debugging.

Because Inngest is built on top of a managed event store, you can focus on business logic instead of scaling workers. The SDK automatically serialises your TypeScript payloads, persists them, and re‑invokes the appropriate function when the event is ready.

Core Concepts

Events

An event is a JSON‑serialisable object that describes something that happened in your system – a user signing up, an order being placed, or a file landing in S3. Events have a name, a data payload, and optional metadata like timestamps or correlation IDs.

Functions (or “Inngest Functions”)

Inngest functions are plain TypeScript functions that react to one or more event names. The SDK registers them with the Inngest platform, which then routes matching events to the correct handler. A function can be synchronous or asynchronous, and you can chain multiple steps inside a single function for complex workflows.

Steps & Durable Execution

Steps let you break a workflow into discrete, retry‑able units. Each step receives the output of the previous step, and Inngest persists the state after every step. If a step fails, only that step is retried – the rest of the workflow resumes where it left off.

This model is similar to Durable Functions or Temporal, but with far less boilerplate and zero self‑hosting.

Getting Started: Installation & Configuration

First, create a fresh Node.js project and install the SDK. Inngest works with any bundler, but the examples below assume a standard npm setup.

npm init -y
npm install inngest
npm install -D typescript @types/node
npx tsc --init

Next, grab an API key from the Inngest dashboard and add it to a .env file. The SDK reads INNGEST_SIGNING_KEY automatically.

# .env
INNGEST_SIGNING_KEY=sk_test_XXXXXXXXXXXXXXXXXXXXXXXX

Finally, create a small bootstrap file that registers your functions with the Inngest client. This file will be imported by your serverless runtime (e.g., Vercel, Netlify, or a plain Express server).

import { Inngest } from "inngest";

export const inngest = new Inngest({
  // The name of your app – appears in the UI
  name: "codeyaan-demo",
});

Example 1: A Simple “Hello, Event!” Function

Let’s start with the classic “Hello, World” but in an event‑driven fashion. We’ll emit an event called greeting/triggered and have Inngest log a friendly message.

Step 1: Define the Event

export const greetEvent = inngest.createEvent({
  name: "greeting/triggered",
  // Optional: schema validation can be added later
});

Step 2: Register the Function

import { inngest } from "./inngest";

export const greetFunction = inngest.createFunction(
  { name: "greeting/handler" },
  { event: "greeting/triggered" },
  async ({ event }) => {
    console.log(`👋 Received greeting: ${event.data.message}`);
  }
);

Step 3: Emit the Event

You can fire the event from anywhere in your code – an API route, a cron job, or even a client‑side fetch.

import { greetEvent } from "./greet";

await greetEvent.send({
  message: "Hello, Inngest!",
});

When you run the code, the Inngest UI will show the event, the function invocation, and the console output. No queues, no Lambda deployments, just pure TypeScript.

Real‑World Use Case #1: E‑Commerce Order Processing

Imagine an online store that needs to orchestrate several steps after a customer places an order: reserve inventory, charge the payment gateway, send a confirmation email, and finally update analytics. Each step can fail independently, and you want automatic retries without duplicating work.

Define the Order Event

export const orderCreated = inngest.createEvent({
  name: "order/created",
});

Orchestrate the Workflow

import { inngest } from "./inngest";
import { reserveInventory } from "./inventory";
import { chargeCustomer } from "./payments";
import { sendConfirmation } from "./email";
import { trackAnalytics } from "./analytics";

export const orderWorkflow = inngest.createFunction(
  { name: "order/workflow" },
  { event: "order/created" },
  async ({ step, event }) => {
    // Step 1: Reserve inventory
    const reservation = await step.run("reserve-inventory", async () => {
      return await reserveInventory(event.data.orderId, event.data.items);
    });

    // Step 2: Charge the payment method
    const charge = await step.run("charge-payment", async () => {
      return await chargeCustomer(event.data.paymentInfo, reservation.total);
    });

    // Step 3: Send confirmation email
    await step.run("send-email", async () => {
      await sendConfirmation(event.data.email, {
        orderId: event.data.orderId,
        items: event.data.items,
        total: reservation.total,
      });
    });

    // Step 4: Update analytics (fire‑and‑forget)
    await step.run("track-analytics", async () => {
      await trackAnalytics("order_completed", {
        orderId: event.data.orderId,
        revenue: reservation.total,
      });
    });

    return { success: true };
  }
);

Notice how each step.run call is isolated. If the payment gateway times out, only the “charge‑payment” step is retried, while the inventory reservation remains intact. Inngest also guarantees exactly‑once execution for idempotent services, which we’ll discuss later.

Emitting the Order Event

await orderCreated.send({
  orderId: "ord_12345",
  items: [{ sku: "TSHIRT-XL", qty: 2 }],
  paymentInfo: { token: "tok_abcdef" },
  email: "customer@example.com",
});

Real‑World Use Case #2: Chat Application Notifications

Realtime chat apps often need to push notifications to mobile devices, email, and Slack when a message arrives. Instead of coupling the notification logic to the WebSocket server, you can delegate it to Inngest.

Message Event Schema

export const messageSent = inngest.createEvent({
  name: "chat/message.sent",
});

Notification Function

import { inngest } from "./inngest";
import { pushToFCM } from "./fcm";
import { sendEmail } from "./email";
import { postToSlack } from "./slack";

export const notifyOnMessage = inngest.createFunction(
  { name: "chat/notify" },
  { event: "chat/message.sent" },
  async ({ step, event }) => {
    const { messageId, content, channelId, mentions } = event.data;

    // Push notification to mentioned users
    await step.run("push-fcm", async () => {
      await pushToFCM(mentions, {
        title: "New mention",
        body: content.slice(0, 100),
        data: { messageId, channelId },
      });
    });

    // Email fallback for offline users
    await step.run("email-fallback", async () => {
      await sendEmail(mentions, {
        subject: "You were mentioned in a chat",
        html: `

${content}

`, }); }); // Optional: post a summary to a Slack channel for monitoring await step.run("slack-summary", async () => { await postToSlack("#chat-monitor", { text: `Message ${messageId} in ${channelId} mentioned ${mentions.length} users.`, }); }); return { notified: true }; } );

This decouples the real‑time socket layer from heavy notification work, improves latency, and gives you a single place to monitor failures.

Advanced Patterns

Retries & Backoff

Inngest automatically retries failed steps with exponential backoff up to a configurable limit. You can customise the retry policy per step:

await step.run(
  "charge-payment",
  async () => await chargeCustomer(...),
  { retry: { maxAttempts: 5, backoff: "exponential" } }
);

If you need a custom delay, you can throw new inngest.RetryAfter(seconds) to tell the platform to pause for a specific interval.

Idempotency

Because steps may be retried, any external call should be idempotent. A common pattern is to include the Inngest event.id as a deduplication key when writing to databases or third‑party APIs.

await step.run("store-order", async () => {
  await db.orders.insert({
    id: event.id, // guarantees uniqueness
    ...event.data,
  });
});

Rate Limiting & Concurrency Control

If you have a downstream API that enforces a request‑per‑second quota, you can use the step.run concurrency options:

await step.run(
  "call-external-api",
  async () => await externalApi.call(...),
  { concurrency: { limit: 10 } }
);

This tells Inngest to schedule at most ten of those steps in parallel, automatically throttling excess calls.

Testing & Debugging Inngest Functions

Inngest ships with a local dev server that mimics the cloud environment. Run npx inngest dev in a separate terminal; it watches your source files, hot‑reloads functions, and provides a UI at http://localhost:8288.

For unit testing, you can invoke a function directly with a mock event payload. The SDK exposes the handler as a plain async function, making it trivial to test with Jest or Vitest.

import { orderWorkflow } from "./orderWorkflow";

test("order workflow succeeds", async () => {
  const mockEvent = {
    id: "evt_test",
    name: "order/created",
    data: {
      orderId: "ord_1",
      items: [{ sku: "BOOK", qty: 1 }],
      paymentInfo: { token: "tok_123" },
      email: "test@example.com",
    },
  };

  const result = await orderWorkflow.handler({ event: mockEvent, step: mockStep });
  expect(result.success).toBe(true);
});

The mockStep object can be a simple stub that records each run call, letting you assert that all expected steps were executed.

Pro tip: Use the Inngest UI’s “Replay” button to re‑run a failed event with the same payload. It’s invaluable when you fix a bug and want to verify the correction without re‑creating the whole upstream trigger.

Performance & Cost Considerations

Since Inngest stores every event, you pay for both event ingestion and function execution. The platform is priced per million events, which is usually cheaper than running a dedicated queue service plus compute. For high‑throughput workloads, batch events together or use a “bulk” event type to reduce overhead.

Latency is typically sub‑second for simple steps, but complex workflows that involve external APIs will be bound by those APIs’ response times. Inngest’s step‑level retries mean you don’t need to implement your own back‑off loops, which reduces code complexity and improves overall reliability.

Deploying Inngest Functions

Because Inngest functions are just regular TypeScript files, you can deploy them on any serverless platform that supports Node.js. The most common patterns are:

  • Vercel Edge Functions: Create a file under /api/inngest that exports the Inngest client and all functions.
  • Netlify Functions: Use the netlify/functions directory and expose a single handler that forwards to Inngest.
  • Express / Fastify: Mount the Inngest HTTP handler on a route, e.g., app.post("/api/inngest", inngest.handler()).

All you need to ensure is that the endpoint is reachable from the Inngest backend (public URL or via a tunnel like ngrok during development).

Security Best Practices

Inngest signs every webhook request with a HMAC using your signing key. Verify the signature in your HTTP handler to prevent spoofed events. The SDK does this automatically if you use inngest.handler(), but custom servers should call inngest.verifyRequest(req) before processing.

Never store raw secrets in the event payload. Instead, reference a secret identifier and fetch the value from a secret manager (AWS Secrets Manager, GCP Secret Manager, etc.) inside the step.

Pro tip: Leverage Inngest’s built‑in “metadata” field to attach correlation IDs that trace a request across multiple services. This makes debugging across microservices much easier.

Monitoring & Observability

The Inngest dashboard provides a timeline view of each event, step status, retry count, and execution duration. You can also export logs to external services via the “log sink” integration (Datadog, New Relic, etc.). For programmatic monitoring, query the Events API to fetch metrics like “events processed per minute” or “failed steps”.

Combine this with custom metrics emitted from your steps (e.g., using Prometheus) to get a full picture of both platform‑level and application‑level health.

Common Pitfalls & How to Avoid Them

  • Unbounded Payloads: Large blobs (e.g., images) should be
Share this article