| name | inngest-durable-functions |
| description | Use when building functions that must survive process crashes, retry automatically on failure, run on a schedule, react to events, or maintain state across infrastructure failures โ e.g., webhook handlers that drop events, flaky cron jobs, background jobs that fail mid-execution, or workflows that need to resume where they left off. Covers Inngest function configuration, triggers (events, cron, invoke), step execution and memoization, idempotency, cancellation, error handling, retries, logging, and observability. |
Inngest Durable Functions
Master Inngest's durable execution model for building fault-tolerant, long-running workflows. This skill covers the complete lifecycle from triggers to error handling.
These skills are focused on TypeScript. For Python or Go, refer to the Inngest documentation for language-specific guidance. Core concepts apply across all languages.
Core Concepts You Need to Know
Durable Execution Model
- Each step should encapsulate side-effects and non-deterministic code
- Memoization prevents re-execution of completed steps
- State persistence survives infrastructure failures
- Automatic retries with configurable retry count
Step Execution Flow
async ({ event, step }) => {
const timestamp = Date.now();
const result = await step.run("process-data", () => {
return processData(event.data);
});
};
async ({ event, step }) => {
const result = await step.run("process-with-timestamp", () => {
const timestamp = Date.now();
return processData(event.data, timestamp);
});
};
Function Limits
Every Inngest function has these hard limits:
- Maximum 1,000 steps per function run
- Maximum 4MB output per individual step (each
step.run() return value)
- Maximum 32MB total persisted run state (event payload, all memoized step outputs, and final function output combined)
- In v4's checkpointing execution model, completed steps are memoized in run state โ they are not necessarily a separate HTTP request on every replay (~50-100ms overhead still applies when a step actually executes)
If you're hitting these limits, break your function into smaller functions connected via step.invoke() or step.sendEvent().
When to Use Steps
Always wrap in step.run():
- API calls and network requests
- Database reads and writes
- File I/O operations
- Any non-deterministic operation
- Anything you want retried independently on failure
Never wrap in step.run():
- Pure calculations and data transformations
- Simple validation logic
- Deterministic operations with no side effects
- Logging (use outside steps)
Function Creation
Basic Function Structure
const processOrder = inngest.createFunction(
{
id: "process-order",
triggers: [{ event: "order/created" }],
retries: 4,
concurrency: 10,
},
async ({ event, step }) => {
},
);
Step IDs and Memoization
const data = await step.run("fetch-data", () => fetchUserData());
const more = await step.run("fetch-data", () => fetchOrderData());
await step.run("validate-payment", () => validatePayment(event.data.paymentId));
await step.run("charge-customer", () => chargeCustomer(event.data));
await step.run("send-confirmation", () => sendEmail(event.data.email));
Triggers and Events
Event Triggers
Triggers are defined in the triggers array in the first argument of createFunction:
inngest.createFunction(
{ id: "my-fn", triggers: [{ event: "user/signup" }] },
async ({ event }) => {
},
);
inngest.createFunction(
{
id: "my-fn",
triggers: [
{
event: "user/action",
if: 'event.data.action == "purchase" && event.data.amount > 100',
},
],
},
async ({ event }) => {
},
);
inngest.createFunction(
{
id: "my-fn",
triggers: [
{ event: "user/signup" },
{ event: "user/login", if: "event.data.firstLogin == true" },
{ cron: "0 9 * * *" },
],
},
async ({ event }) => {
},
);
Cron Triggers
inngest.createFunction(
{ id: "my-fn", triggers: [{ cron: "0 */6 * * *" }] },
async ({ step }) => {
},
);
inngest.createFunction(
{ id: "my-fn", triggers: [{ cron: "TZ=Europe/Paris 0 12 * * 5" }] },
async ({ step }) => {
},
);
inngest.createFunction(
{
id: "my-fn",
triggers: [
{ event: "manual/report.requested" },
{ cron: "0 0 * * 0" },
],
},
async ({ event, step }) => {
},
);
Function Invocation
const result = await step.invoke("generate-report", {
function: generateReportFunction,
data: { userId: event.data.userId },
});
await step.run("process-report", () => {
return processReport(result);
});
Idempotency Strategies
Event-Level Idempotency (Producer Side)
await inngest.send({
id: `checkout-completed-${cartId}`,
name: "cart/checkout.completed",
data: { cartId, email: "user@example.com" },
});
Function-Level Idempotency (Consumer Side)
const sendEmail = inngest.createFunction(
{
id: "send-checkout-email",
triggers: [{ event: "cart/checkout.completed" }],
idempotency: "event.data.cartId",
},
async ({ event, step }) => {
},
);
const processUserAction = inngest.createFunction(
{
id: "process-user-action",
triggers: [{ event: "user/action.performed" }],
idempotency: 'event.data.userId + "-" + event.data.organizationId',
},
async ({ event, step }) => {
},
);
Cancellation Patterns
Event-Based Cancellation
In expressions, event = the original triggering event, async = the new event being matched. See Expression Syntax Reference for full details.
const processOrder = inngest.createFunction(
{
id: "process-order",
triggers: [{ event: "order/created" }],
cancelOn: [
{
event: "order/cancelled",
if: "event.data.orderId == async.data.orderId",
},
],
},
async ({ event, step }) => {
await step.sleepUntil("wait-for-payment", event.data.paymentDue);
await step.run("charge-payment", () => processPayment(event.data));
},
);
Timeout Cancellation
const processWithTimeout = inngest.createFunction(
{
id: "process-with-timeout",
triggers: [{ event: "long/process.requested" }],
timeouts: {
start: "5m",
finish: "30m",
},
},
async ({ event, step }) => {
},
);
Handling Cancellation Cleanup
const cleanupCancelled = inngest.createFunction(
{
id: "cleanup-cancelled-process",
triggers: [{ event: "inngest/function.cancelled" }],
},
async ({ event, step }) => {
if (event.data.function_id === "process-order") {
await step.run("cleanup-resources", () => {
return cleanupOrderResources(event.data.run_id);
});
}
},
);
Error Handling and Retries
Default Retry Behavior
- 5 total attempts (1 initial + 4 retries) per step
- Exponential backoff with jitter
- Independent retry counters per step
Custom Retry Configuration
const reliableFunction = inngest.createFunction(
{
id: "reliable-function",
triggers: [{ event: "critical/task" }],
retries: 10,
},
async ({ event, step, attempt }) => {
if (attempt > 5) {
}
},
);
Non-Retriable Errors
Prevent retries for code that won't succeed upon retry.
import { NonRetriableError } from "inngest";
const processUser = inngest.createFunction(
{ id: "process-user", triggers: [{ event: "user/process.requested" }] },
async ({ event, step }) => {
const user = await step.run("fetch-user", async () => {
const user = await db.users.findOne(event.data.userId);
if (!user) {
throw new NonRetriableError("User not found, stopping execution");
}
return user;
});
},
);
Custom Retry Timing
import { RetryAfterError } from "inngest";
const respectRateLimit = inngest.createFunction(
{ id: "api-call", triggers: [{ event: "api/call.requested" }] },
async ({ event, step }) => {
await step.run("call-api", async () => {
const response = await externalAPI.call(event.data);
if (response.status === 429) {
const retryAfter = response.headers["retry-after"];
throw new RetryAfterError("Rate limited", `${retryAfter}s`);
}
return response.data;
});
},
);
Logging Best Practices
Proper Logging Setup
import winston from "winston";
const logger = winston.createLogger({
level: "info",
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
const inngest = new Inngest({
id: "my-app",
logger,
});
import { ConsoleLogger, Inngest } from "inngest";
const inngest = new Inngest({
id: "my-app",
logger: new ConsoleLogger({ level: "debug" }),
});
โ ๏ธ v4 Breaking Change: The logLevel option has been removed. Use the logger option with ConsoleLogger or a custom logger instead.
Function Logging Patterns
const processData = inngest.createFunction(
{ id: "process-data", triggers: [{ event: "data/process.requested" }] },
async ({ event, step, logger }) => {
const result = await step.run("fetch-data", async () => {
logger.info("Fetching data for user", { userId: event.data.userId });
return await fetchUserData(event.data.userId);
});
await step.run("log-completion", async () => {
logger.info("Processing complete", { resultCount: result.length });
});
},
);
Performance Optimization
Checkpointing
Checkpointing is enabled by default in v4. It allows functions to persist state periodically during execution, reducing latency between steps.
const realTimeFunction = inngest.createFunction(
{
id: "real-time-function",
triggers: [{ event: "realtime/process" }],
checkpointing: {
maxRuntime: "50s",
},
},
async ({ event, step }) => {
const result1 = await step.run("step-1", () => process1(event.data));
const result2 = await step.run("step-2", () => process2(result1));
return { result2 };
},
);
const legacyFunction = inngest.createFunction(
{
id: "legacy-function",
triggers: [{ event: "legacy/process" }],
checkpointing: false,
},
async ({ event, step }) => {
},
);
Advanced Patterns
Conditional Step Execution
const conditionalProcess = inngest.createFunction(
{ id: "conditional-process", triggers: [{ event: "process/conditional" }] },
async ({ event, step }) => {
const userData = await step.run("fetch-user", () => {
return getUserData(event.data.userId);
});
if (userData.isPremium) {
await step.run("premium-processing", () => {
return processPremiumFeatures(userData);
});
}
await step.run("standard-processing", () => {
return processStandardFeatures(userData);
});
},
);
Error Recovery Patterns
const robustProcess = inngest.createFunction(
{ id: "robust-process", triggers: [{ event: "process/robust" }] },
async ({ event, step }) => {
let primaryResult;
try {
primaryResult = await step.run("primary-service", () => {
return callPrimaryService(event.data);
});
} catch (error) {
primaryResult = await step.run("fallback-service", () => {
return callSecondaryService(event.data);
});
}
return { result: primaryResult };
},
);
Common Mistakes to Avoid
- โ Non-deterministic code outside steps
- โ Database calls outside steps
- โ Logging outside steps (causes duplicates)
- โ Changing step IDs after deployment
- โ Not handling NonRetriableError cases
- โ Ignoring idempotency for critical functions
Next Steps
This skill covers Inngest's durable function patterns. For event sending and webhook handling, see the inngest-events skill.
This Repository
These upstream Inngest instructions are vendored for agent tooling and
integration work in this monorepo.
Repository Triggers
Use this skill when inngest-durable-functions matches the current Inngest task. If the
right skill is unclear, start with docs/ai/skills/inngest/SKILL.md.
Repository Workflow
- Confirm whether the request is agent-tooling guidance or product runtime
integration.
- Use
inngest-brownfield-audit before changing existing app workflows or
fragile background work.
- Follow this upstream guidance under OpenSpec, root
AGENTS.md, repo
rulebooks, framework docs, and runtime evidence.
- Keep runtime packages, app code, migrations, and
INNGEST_* env
requirements out of agent-tooling-only changes.
Repository Checklist