Fix: Inngest Not Working — Functions Not Triggering, Steps Failing, or Events Not Received
Quick Answer
How to fix Inngest issues — function and event setup, step orchestration, retries and error handling, cron scheduling, concurrency control, fan-out patterns, and local development with the Dev Server.
The Problem
An Inngest function is defined but never runs:
const myFunction = inngest.createFunction(
{ id: 'process-order' },
{ event: 'order/created' },
async ({ event, step }) => {
console.log('Processing order', event.data.orderId);
}
);
// Event is sent but the function never executesOr steps fail silently and retry forever:
async ({ event, step }) => {
const result = await step.run('fetch-data', async () => {
return fetch('/api/data').then(r => r.json());
});
// Step fails, retries 4 times, then the function fails
}Or the Dev Server shows “No functions found”:
npx inngest-cli@latest dev
# Dev Server: http://localhost:8288
# No functions registeredWhy This Happens
Inngest is a durable function execution engine. Functions are triggered by events and can run multi-step workflows that survive failures:
- Functions must be served via an HTTP endpoint — Inngest calls your functions by sending HTTP requests to your app. If the
/api/inngestroute isn’t set up, or the Dev Server can’t reach your app, functions are registered but never invoked. - The serve handler registers functions with Inngest — you must call
serve()with your Inngest client and function list, and expose it as an API route. This handler both registers functions (on sync) and receives execution requests. - Steps are individually retriable units — each
step.run()call is its own HTTP request. If a step throws, only that step retries (not the whole function). A step’s return value is memoized — after a successful run, it won’t re-execute on subsequent retries. - Events must match the trigger exactly — the
eventfield increateFunction’s trigger must match thenamepassed toinngest.send(). A typo means the event fires but no function listens for it.
Fix 1: Set Up Inngest with Next.js
npm install inngest// src/inngest/client.ts — create the Inngest client
import { Inngest } from 'inngest';
export const inngest = new Inngest({
id: 'my-app',
// Optional: type your events for full TypeScript support
});// src/inngest/functions.ts — define functions
import { inngest } from './client';
// Function triggered by an event
export const processOrder = inngest.createFunction(
{
id: 'process-order',
retries: 3, // Retry failed steps up to 3 times
},
{ event: 'order/created' },
async ({ event, step }) => {
// Step 1: Validate order
const order = await step.run('validate-order', async () => {
const res = await fetch(`https://api.example.com/orders/${event.data.orderId}`);
if (!res.ok) throw new Error('Order not found');
return res.json();
});
// Step 2: Charge payment
const payment = await step.run('charge-payment', async () => {
return processPayment(order.total, order.paymentMethod);
});
// Step 3: Send confirmation email
await step.run('send-confirmation', async () => {
await sendEmail({
to: order.email,
template: 'order-confirmation',
data: { orderId: order.id, total: order.total },
});
});
return { orderId: order.id, paymentId: payment.id, status: 'completed' };
},
);
// Cron-triggered function
export const dailyCleanup = inngest.createFunction(
{ id: 'daily-cleanup' },
{ cron: '0 3 * * *' }, // Every day at 3 AM UTC
async ({ step }) => {
await step.run('delete-expired-sessions', async () => {
await db.delete(sessions).where(lt(sessions.expiresAt, new Date()));
});
await step.run('archive-old-logs', async () => {
await db.delete(logs).where(lt(logs.createdAt, subDays(new Date(), 30)));
});
},
);// app/api/inngest/route.ts — Next.js App Router
import { serve } from 'inngest/next';
import { inngest } from '@/inngest/client';
import { processOrder, dailyCleanup } from '@/inngest/functions';
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [processOrder, dailyCleanup],
// All functions must be listed here
});// Send events from anywhere in your app
import { inngest } from '@/inngest/client';
// In a Server Action, API route, or route handler
export async function createOrder(data: OrderInput) {
const order = await db.insert(orders).values(data).returning();
// Trigger the Inngest function
await inngest.send({
name: 'order/created',
data: {
orderId: order[0].id,
userId: data.userId,
total: data.total,
},
});
return order[0];
}Fix 2: Step Orchestration Patterns
// Wait for an external event (webhook, user action)
export const onboardingFlow = inngest.createFunction(
{ id: 'user-onboarding' },
{ event: 'user/signed-up' },
async ({ event, step }) => {
// Send welcome email immediately
await step.run('send-welcome', async () => {
await sendEmail({ to: event.data.email, template: 'welcome' });
});
// Wait up to 24 hours for user to verify email
const verifyEvent = await step.waitForEvent('wait-for-verify', {
event: 'user/email-verified',
match: 'data.userId', // Match by userId
timeout: '24h',
});
if (!verifyEvent) {
// Timed out — send reminder
await step.run('send-reminder', async () => {
await sendEmail({ to: event.data.email, template: 'verify-reminder' });
});
return { status: 'reminder-sent' };
}
// Email verified — continue onboarding
await step.run('setup-defaults', async () => {
await createDefaultSettings(event.data.userId);
});
return { status: 'onboarding-complete' };
},
);
// Sleep / delay between steps
export const drip = inngest.createFunction(
{ id: 'drip-campaign' },
{ event: 'user/trial-started' },
async ({ event, step }) => {
await step.run('day-0-email', async () => {
await sendEmail({ to: event.data.email, template: 'trial-welcome' });
});
await step.sleep('wait-3-days', '3d');
await step.run('day-3-email', async () => {
await sendEmail({ to: event.data.email, template: 'trial-tips' });
});
await step.sleep('wait-4-more-days', '4d');
await step.run('day-7-email', async () => {
await sendEmail({ to: event.data.email, template: 'trial-ending' });
});
},
);
// Parallel steps (fan-out)
export const batchProcess = inngest.createFunction(
{ id: 'batch-process' },
{ event: 'batch/started' },
async ({ event, step }) => {
const items = event.data.items as string[];
// Run steps in parallel
const results = await Promise.all(
items.map((item, i) =>
step.run(`process-item-${i}`, async () => {
return processItem(item);
})
)
);
await step.run('aggregate-results', async () => {
return aggregateResults(results);
});
},
);Fix 3: Error Handling and Retries
import { NonRetriableError } from 'inngest';
export const riskyFunction = inngest.createFunction(
{
id: 'risky-operation',
retries: 5, // Max retry attempts per step
onFailure: async ({ error, event, step }) => {
// Called after all retries are exhausted
await step.run('notify-failure', async () => {
await sendAlert({
channel: '#errors',
message: `Function failed: ${error.message}`,
event: event.data,
});
});
},
},
{ event: 'risky/started' },
async ({ event, step }) => {
await step.run('check-input', async () => {
if (!event.data.userId) {
// Non-retriable — don't waste retries on bad input
throw new NonRetriableError('userId is required');
}
});
await step.run('call-external-api', async () => {
const res = await fetch('https://api.external.com/process', {
method: 'POST',
body: JSON.stringify({ userId: event.data.userId }),
});
if (res.status === 400) {
// Client error — retrying won't help
throw new NonRetriableError(`Bad request: ${await res.text()}`);
}
if (!res.ok) {
// Server error — retrying might help
throw new Error(`API returned ${res.status}`);
}
return res.json();
});
},
);Fix 4: Concurrency and Throttling
// Limit concurrent executions
export const sendNotification = inngest.createFunction(
{
id: 'send-notification',
concurrency: {
limit: 5, // Max 5 concurrent executions
},
},
{ event: 'notification/send' },
async ({ event, step }) => {
await step.run('send', async () => {
await pushNotification(event.data.userId, event.data.message);
});
},
);
// Throttle by key — limit per user, per resource, etc.
export const apiSync = inngest.createFunction(
{
id: 'api-sync',
throttle: {
limit: 1,
period: '1m',
key: 'event.data.userId', // 1 sync per user per minute
},
},
{ event: 'sync/requested' },
async ({ event, step }) => {
await step.run('sync-data', async () => {
await syncUserData(event.data.userId);
});
},
);
// Rate limit with concurrency key
export const processWebhook = inngest.createFunction(
{
id: 'process-webhook',
concurrency: {
limit: 10,
key: 'event.data.tenantId', // 10 concurrent per tenant
},
},
{ event: 'webhook/received' },
async ({ event, step }) => {
// Each tenant gets up to 10 parallel executions
await step.run('process', async () => {
return handleWebhook(event.data);
});
},
);
// Debounce — only run once for repeated events
export const updateSearchIndex = inngest.createFunction(
{
id: 'update-search-index',
debounce: {
period: '10s',
key: 'event.data.documentId',
},
},
{ event: 'document/updated' },
async ({ event, step }) => {
// If the document is updated 5 times in 10 seconds,
// this function only runs once with the latest event
await step.run('reindex', async () => {
await reindexDocument(event.data.documentId);
});
},
);Fix 5: Local Development
# Start the Inngest Dev Server
npx inngest-cli@latest dev
# Opens at http://localhost:8288
# Automatically discovers functions at http://localhost:3000/api/inngest// Custom port or app URL
// npx inngest-cli@latest dev -u http://localhost:5173/api/inngest
// Send test events from the Dev Server UI
// Or programmatically:
import { inngest } from '@/inngest/client';
// In a test file or script
await inngest.send({
name: 'order/created',
data: {
orderId: 'test-123',
userId: 'user-456',
total: 99.99,
},
});Dev Server shows “No functions found”:
- Your app must be running (
npm run dev) - The Dev Server must be able to reach your app’s serve endpoint
- Check the serve endpoint returns function metadata on GET
- Verify the URL — default is
http://localhost:3000/api/inngest
Fix 6: Typed Events
// src/inngest/client.ts — full type safety
import { Inngest, EventSchemas } from 'inngest';
// Define event types
type Events = {
'order/created': {
data: {
orderId: string;
userId: string;
total: number;
items: Array<{ productId: string; quantity: number }>;
};
};
'order/shipped': {
data: {
orderId: string;
trackingNumber: string;
carrier: 'fedex' | 'ups' | 'usps';
};
};
'user/signed-up': {
data: {
userId: string;
email: string;
plan: 'free' | 'pro' | 'enterprise';
};
};
'user/email-verified': {
data: {
userId: string;
};
};
};
export const inngest = new Inngest({
id: 'my-app',
schemas: new EventSchemas().fromRecord<Events>(),
});
// Now events and function triggers are fully typed
// inngest.send({ name: 'order/created', data: { orderId: '...' } })
// ^^^^^^^^ — TypeScript checks thisStill Not Working?
Function registered but never triggers — the event name must match exactly. inngest.send({ name: 'order/created' }) only triggers functions listening for 'order/created', not 'order.created' or 'Order/Created'. Check the Dev Server’s event stream to see if the event was received.
Steps re-execute after a failure — this is by design. When a step fails and the function retries, previously successful steps are skipped (their return values are memoized). Only the failed step re-runs. If you see all steps re-running, each step might be throwing, or the function is failing before reaching the step that needs retry.
Dev Server can’t find functions — the Dev Server makes a GET request to your serve endpoint to discover functions. If your app is on a non-standard port, pass it explicitly: npx inngest-cli dev -u http://localhost:5173/api/inngest. Also ensure the route handler exports GET, POST, and PUT.
“Event key is required” in production — when deploying to Inngest Cloud, you need an event key set as INNGEST_EVENT_KEY and a signing key as INNGEST_SIGNING_KEY. These are available in the Inngest dashboard after creating an app. Without them, the client can’t authenticate with the Inngest servers.
For related serverless issues, see Fix: Wrangler Not Working and Fix: Nitro Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Trigger.dev Not Working — Tasks Not Running, Runs Failing, or Dev Server Not Connecting
How to fix Trigger.dev issues — task definition and triggering, dev server setup, scheduled tasks with cron, concurrency and queues, retries, idempotency, and deployment to Trigger.dev Cloud.
Fix: SST Not Working — Deploy Failing, Bindings Not Linking, or Lambda Functions Timing Out
How to fix SST (Serverless Stack) issues — resource configuration with sst.config.ts, linking resources to functions, local dev with sst dev, database and storage setup, and deployment troubleshooting.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Better Auth Not Working — Login Failing, Session Null, or OAuth Callback Error
How to fix Better Auth issues — server and client setup, email/password and OAuth providers, session management, middleware protection, database adapters, and plugin configuration.