Inngest
Integrate Inngest with your application for event-driven background workflows.
Inngest is a developer-first platform for building reliable background jobs, scheduled functions, and event-driven workflows. It provides a simple API for creating durable functions that automatically retry on failure.
This integration is not installed by default
The starter kits do not include the Inngest SDK, credentials, functions or API route. This page is an optional implementation recipe. Add it only when its event-driven execution model fits your product.
Why Inngest?
Inngest makes it easy to build reliable background jobs with automatic retries, scheduling, and event-driven workflows. Functions are defined in your codebase and run on Inngest's infrastructure, giving you the reliability of a queue with the simplicity of writing regular functions.
Setup
Visit Inngest and create a free account. Create a new app and note down your signing key.
Add your Inngest credentials to your environment variables:
INNGEST_EVENT_KEY=your_event_key_here
INNGEST_SIGNING_KEY=your_signing_key_hereRegister both server-only variables in lib/env.ts so the client example below
typechecks and missing production credentials fail validation:
server: {
// Existing variables...
+ INNGEST_EVENT_KEY: z.string().min(1),
+ INNGEST_SIGNING_KEY: z.string().min(1)
}Expose them to the validator in the same file:
runtimeEnv: {
// Existing variables...
+ INNGEST_EVENT_KEY: process.env.INNGEST_EVENT_KEY,
+ INNGEST_SIGNING_KEY: process.env.INNGEST_SIGNING_KEY
}Keep these values server-only and use separate Inngest environments and keys for development, previews and production.
Install dependencies
This recipe uses the stable Inngest TypeScript SDK v3 API. Pin that major so a future v4 upgrade does not silently invalidate the examples:
npm install inngest@^3Configure Inngest
Create an Inngest client:
import { EventSchemas, Inngest } from 'inngest';
import * as z from 'zod';
const schemas = new EventSchemas().fromSchema({
'user/data.process': z.object({
userId: z.string(),
operation: z.enum(['export', 'analyze', 'cleanup'])
})
});
export const inngest = new Inngest({
id: 'your-app-id',
schemas
});The SDK reads INNGEST_EVENT_KEY from the server environment when sending and
the Next.js handler uses INNGEST_SIGNING_KEY to authenticate incoming
requests. The event schema gives inngest.send() and event.data one shared
contract with runtime validation.
Create your first function
Create functions in a lib/inngest/functions directory:
import { inngest } from '@/lib/inngest';
export const processUserData = inngest.createFunction(
{ id: 'process-user-data' },
{ event: 'user/data.process' },
async ({ event, step }) => {
const { userId, operation } = event.data;
await step.run('process-data', async () => {
console.log('Processing user data', { userId, operation });
switch (operation) {
case 'export':
// Export user data
await new Promise((resolve) => setTimeout(resolve, 2000));
return { success: true, result: 'Data exported to CSV' };
case 'analyze':
// Analyze user data
await new Promise((resolve) => setTimeout(resolve, 5000));
return {
success: true,
result: { totalActions: 156, avgSessionTime: '4m 32s' }
};
case 'cleanup':
// Cleanup user data
await new Promise((resolve) => setTimeout(resolve, 3000));
return { success: true, result: 'Removed 23 obsolete records' };
default:
throw new Error(`Unknown operation: ${operation}`);
}
});
}
);Create a scheduled function:
import { inngest } from '@/lib/inngest';
export const dailyCleanup = inngest.createFunction(
{ id: 'daily-cleanup' },
{ cron: '0 2 * * *' }, // Daily at 2 AM
async ({ step }) => {
await step.run('cleanup-logs', async () => {
console.log('Cleaning up old logs');
await new Promise((resolve) => setTimeout(resolve, 5000));
return { logsCleaned: true };
});
await step.run('cleanup-temp-files', async () => {
console.log('Cleaning up temporary files');
await new Promise((resolve) => setTimeout(resolve, 3000));
return { tempFilesCleaned: true };
});
await step.run('generate-reports', async () => {
console.log('Generating daily reports');
await new Promise((resolve) => setTimeout(resolve, 8000));
return { reportsGenerated: true };
});
}
);Register functions
Create an API route to serve your Inngest functions:
import { serve } from 'inngest/next';
import { inngest } from '@/lib/inngest';
import { dailyCleanup } from '@/lib/inngest/functions/daily-cleanup';
import { processUserData } from '@/lib/inngest/functions/process-user-data';
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [processUserData, dailyCleanup]
});Triggering functions
From an API route
import { NextRequest, NextResponse } from 'next/server';
import * as z from 'zod';
import { getSession } from '@/lib/auth/server';
import { inngest } from '@/lib/inngest';
const processUserDataSchema = z.object({
operation: z.enum(['export', 'analyze', 'cleanup'])
});
export async function POST(request: NextRequest) {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { operation } = processUserDataSchema.parse(body);
await inngest.send({
name: 'user/data.process',
data: { userId: session.user.id, operation }
});
return NextResponse.json({
success: true,
message: 'Background task started successfully'
});
}From a server action
'use server';
import { getSession } from '@/lib/auth/server';
import { inngest } from '@/lib/inngest';
export async function processUserData(
operation: 'export' | 'analyze' | 'cleanup'
) {
const session = await getSession();
if (!session) {
throw new Error('Unauthorized');
}
try {
await inngest.send({
name: 'user/data.process',
data: { userId: session.user.id, operation }
});
return { success: true };
} catch (error) {
console.error('Failed to trigger background task:', error);
throw new Error('Failed to start background task');
}
}The authenticated user ID is derived from the server session. Do not accept a user or organization owner from the browser and treat it as authorization. For organization work, verify membership before publishing and send only a stored job ID or the minimum identifiers the worker needs.
Monitoring and debugging
Visit the Inngest Dashboard to monitor your functions:
- View function execution logs and performance metrics
- Track success and failure rates
- Monitor function duration and step execution
- Replay failed functions
- Set up alerts for function failures
Best practices
Use step functions for reliability
Break your function into steps using step.run() to make it more reliable and debuggable:
await step.run('step-name', async () => {
// This step will be retried independently if it fails
return await processData();
});Use descriptive function IDs
// ✅ Good
{
id: 'user-data-export-csv';
}
// ❌ Not so good
{
id: 'task1';
}Handle errors gracefully
await step.run('process', async () => {
try {
return await processData();
} catch (error) {
console.error('Processing failed:', error);
throw error; // Re-throw to trigger retry
}
});Next steps
With Inngest integrated into your application, you can now:
- Build reliable background jobs with automatic retries
- Schedule recurring tasks with cron expressions
- Create event-driven workflows that respond to events
- Compose complex workflows using step functions
Ready to explore more? Check out the official documentation for advanced features like function composition, event filtering, and more.