Tutorials / Backend integrations / Background jobs with Inngest
📝 Written ● Intermediate Updated 2026-05-13

Background jobs with Inngest

When your server needs to "do this later," you used to set up Redis, BullMQ, a worker process, and a deploy story for all of it. Inngest replaces that with a function and an event. Durable retries built in; no infrastructure to manage; works on serverless.

What this replaces

0

Things that should run after the user's request returns:

  • Send the welcome email after sign-up.
  • Generate a PDF report.
  • Process a video upload.
  • Fan out a notification to many recipients.
  • Sync data to a third-party API and retry if it fails.

Classic stack: Redis + BullMQ + a worker process. Modern serverless answer: Inngest, Trigger.dev ↗, or Temporal ↗. This tutorial uses Inngest because the developer experience is the most beginner-friendly.

Sign up + create an app

1

Sign up at app.inngest.com ↗. Free tier: 100K events/month + 1K runs concurrency. Real for early-stage projects.

Dashboard → Apps → your app gets created automatically when you first connect. We'll come back to this.

Install the SDK

2
npm install inngest

Other runtimes: Python ↗, Go ↗. JS examples below.

Define your first function

3

Inngest functions are plain async functions, triggered by named events.

// inngest/functions.js
import { Inngest } from "inngest";

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

export const sendWelcomeEmail = inngest.createFunction(
  { id: "send-welcome-email" },
  { event: "user/signed-up" },
  async ({ event, step }) => {
    const { email } = event.data;

    // Each `step.run` is durable — retried independently on failure
    const customer = await step.run("create-customer", async () => {
      return createStripeCustomer(email);
    });

    await step.run("send-email", async () => {
      return sendResendEmail({
        to: email,
        subject: "Welcome",
        body: `Hi! Customer ID: ${customer.id}`,
      });
    });
  }
);

Mount the handler

4

Add an HTTP endpoint where Inngest calls your functions. Framework-specific:

Next.js (App Router):

// app/api/inngest/route.ts
import { serve } from "inngest/next";
import { inngest, sendWelcomeEmail } from "@/inngest/functions";

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

Express:

import { serve } from "inngest/express";
import express from "express";

const app = express();
app.use(express.json());
app.use("/api/inngest", serve({
  client: inngest,
  functions: [sendWelcomeEmail],
}));

Full framework list: Serving functions ↗. Vercel, Cloudflare Workers, Hono, Fastify, Remix, Astro, NestJS, AWS Lambda — all supported.

Send an event

5

From anywhere in your app — an API route, a webhook handler, a cron job:

import { inngest } from "@/inngest/functions";

await inngest.send({
  name: "user/signed-up",
  data: { email: "[email protected]" },
});

Returns immediately — within a few ms. Your user's request returns fast; Inngest takes over running the function.

Run locally with the dev server

6

Inngest ships a local dev server — Docker-free, runs in your terminal:

npx inngest-cli@latest dev

Opens at http://localhost:8288. The dev server auto-discovers your /api/inngest endpoint, lists your functions, lets you trigger events from a UI, and shows step-by-step execution traces with retry behavior. Real-time debugging without leaving your laptop.

Connect to production

7

Deploy your app. Then in the Inngest dashboard → Apps → Sync new app → paste your production URL ending in /api/inngest. Inngest pings it, discovers your functions, and starts routing events.

Add env vars to production:

INNGEST_EVENT_KEY=...    # for inngest.send() to authenticate
INNGEST_SIGNING_KEY=...  # for Inngest to verify it's calling YOUR app

Both are visible in the dashboard under Settings → Keys.

Powerful patterns (the things you'd build yourself otherwise)

8

Sleep / delay. Wait inside a function for hours or days; Inngest doesn't keep your server warm during the wait.

await step.sleep("wait-a-day", "24h");
await step.run("send-reminder", () => sendEmail(...));
9

Wait for event. Pause until a specific event arrives. Useful for "if user doesn't confirm in 24h, send a reminder."

const confirmation = await step.waitForEvent("wait-for-confirm", {
  event: "user/email-confirmed",
  timeout: "24h",
  match: "data.userId",
});

if (!confirmation) {
  await step.run("send-nudge", () => sendNudgeEmail(...));
}
10

Scheduled (cron).

export const dailyDigest = inngest.createFunction(
  { id: "daily-digest" },
  { cron: "0 8 * * *" },     // every day at 08:00 UTC
  async ({ step }) => {
    await step.run("send-digest", () => sendDigestToAllUsers());
  }
);
11

Fan-out. One event triggers N parallel functions:

// Two functions, both listening for the same event
export const a = inngest.createFunction({ id: "a" }, { event: "order/paid" }, ...);
export const b = inngest.createFunction({ id: "b" }, { event: "order/paid" }, ...);

// One send triggers both
await inngest.send({ name: "order/paid", data: {...} });
12

Concurrency limits + throttling. Limit how many runs execute simultaneously (e.g., for rate-limited third-party APIs):

inngest.createFunction(
  {
    id: "process-image",
    concurrency: { limit: 5 },                  // at most 5 at once globally
    throttle: { limit: 100, period: "1m" },     // and no more than 100/min
  },
  { event: "image/uploaded" },
  async ({ event, step }) => { /* ... */ }
);

Retries are automatic

13

If a step.run throws, Inngest retries it with exponential backoff (default 4 retries over ~10 minutes). Successful steps don't re-run — only the failing step does. Configure per function:

inngest.createFunction(
  { id: "syncs", retries: 10 },
  { event: "user/created" },
  async ({ event, step }) => { ... }
);

Throw NonRetriableError to fail immediately without retries (e.g., invalid input — no point retrying).

Common pitfalls

14
  • Forgetting step.run — code outside step.run re-runs on every retry. Side effects (DB writes, API calls) belong in steps.
  • Big payloads in events — events have a size limit (typically 512 KB). For large data, store in S3/R2 and pass the URL.
  • Long-running steps on Vercel — Vercel kills functions after 10–60s depending on plan. Inngest steps respect that — split work into smaller step.run calls; Inngest will run them as separate invocations.
  • Forgetting to mount the handler — events flow to Inngest fine, but Inngest can't run your functions until it can call your endpoint. Dev server prints helpful errors when it can't reach you.
Inngest is opinionated about the "events are nouns/verbs" pattern. Event names like user/signed-up, order/paid, image/uploaded are the recommended shape. Functions react to events; events are the API. This is good for reasoning but takes a turn to internalize. Events guide ↗.

Pricing reality

15

Free tier: 100K events/month + 1K concurrent runs. Plenty for early projects. Paid starts at $50/mo. Inngest pricing ↗.

Self-hosted: Inngest open-sources a community edition ↗. Production self-hosting is real work.

Official references

What's next