Appearance
Durable Endpoints
Create durable HTTP endpoints using inngest.endpoint(). Each step within the handler is checkpointed, allowing automatic recovery from failures.
ts
import { Inngest, step } from "inngest";
import { endpointAdapter } from "inngest/edge";
const inngest = new Inngest({ id: "my-app", endpointAdapter });
export const handler = inngest.endpoint(async (req: Request): Promise<Response> => {
const data = await step.run("fetch-data", async () => {
return await fetchExternalAPI();
});
return Response.json({ data });
});Setup
endpointAdapter
The endpointAdapter must be passed to the Inngest client constructor to enable Durable Endpoints. Import it from the entry point matching your runtime:
ts
import { Inngest } from "inngest";
import { endpointAdapter } from "inngest/edge";
const inngest = new Inngest({
id: "my-app",
endpointAdapter,
});ts
import { Inngest } from "inngest";
import { endpointAdapter } from "inngest/node";
const inngest = new Inngest({
id: "my-app",
endpointAdapter,
});Callout: The endpointAdapter is required. Without it, inngest.endpoint() will not be available.
endpointAdapter.withOptions(options)
Use withOptions() to customize adapter behavior:
ts
const inngest = new Inngest({
id: "my-app",
endpointAdapter: endpointAdapter.withOptions({
asyncRedirectUrl: "/api/inngest/poll",
retries: 5,
}),
});asyncRedirectUrl(string | ((params: { runId: string; token: string }) => string | Promise<string>)): Custom URL to redirect to when transitioning from sync to async mode. A string path is resolved relative to the request origin and automatically appends runId and token query parameters. A function gives you full control over URL construction.
functionId(string): Override the auto-detected function ID for this endpoint. Defaults to {METHOD} {path}.
retries(number): Maximum retries for all steps in this endpoint. Must be between 0 and 20. Defaults to 3.
asyncResponse("redirect" | "token"): Response type when transitioning from sync to async mode. Defaults to "redirect".
inngest.endpoint(handler): Handler
Creates a durable endpoint handler that can use step primitives for checkpointing.
handler((...args: any[]) => any): A handler function compatible with the framework you're using. This would be the usual function you use for a request handler before wrapping in inngest.endpoint().Within this handler, you can use all step primitives (step.run(), step.sleep(), step.waitForEvent()) for durable execution.
Returns: A request handler for your framework.
ts
export const handler = inngest.endpoint(
async (req: Request): Promise<Response> => {
const url = new URL(req.url);
const id = url.searchParams.get("id");
const result = await step.run("process", async () => {
return await processItem(id);
});
return Response.json({ result });
}
);Available Step Methods
Within a Durable Endpoint handler, you have access to all step methods: see Available Step Methods.
For example:
step.run(id, fn)
Execute and checkpoint a function. If the endpoint is retried, completed steps return their cached result instantly.
ts
const user = await step.run("fetch-user", async () => {
return await db.users.findOne({ id: userId });
});See step.run() reference for full documentation.
step.sleep(id, duration)
Pause execution for a specified duration. The endpoint will be resumed after the sleep completes.
ts
await step.sleep("rate-limit-pause", "30s");See step.sleep() reference for full documentation.
step.waitForEvent(id, options)
Wait for an external event before continuing. Useful for human-in-the-loop workflows.
ts
const approval = await step.waitForEvent("wait-for-approval", {
event: "approval/received",
match: "data.requestId",
timeout: "24h",
});See step.waitForEvent() reference for full documentation.
Passing Data to Endpoints
Callout: POST body is not yet supported. Use query string parameters to pass data to Durable Endpoints. POST body support is coming soon.
ts
export const handler = inngest.endpoint(async (req: Request): Promise<Response> => {
const url = new URL(req.url);
// Read data from query parameters
const userId = url.searchParams.get("userId");
const action = url.searchParams.get("action");
// Process with durable steps
const result = await step.run("process", async () => {
return await processAction(userId, action);
});
return Response.json({ result });
});Returning Responses
However you return data in your framework is compatible with Durable Endpoints.
For example, using a regular Web API request:
ts
// JSON response
return Response.json({ success: true, data: result });
// Text response
return new Response("OK", { status: 200 });
// Error response
return new Response(JSON.stringify({ error: "Not found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});Error Handling
Errors thrown within step.run() will trigger automatic retries. Use standard try/catch for custom error handling:
ts
export const handler = inngest.endpoint(async (req: Request): Promise<Response> => {
try {
const result = await step.run("risky-operation", async () => {
return await riskyAPICall();
});
return Response.json({ result });
} catch (error) {
// All retries exhausted, handle gracefully
return Response.json(
{ error: "Operation failed after retries" },
{ status: 500 }
);
}
});Framework Integration
Durable Endpoints is only available for Bun and Next.js API endpoints.
Reach out on Discord to ask support for additional frameworks.
Requesting a Durable Endpoint
Durable Endpoints behave like regular API endpoints on the success path. You can request them from your front-end (or back-end) using fetch() or your favorite query or http library.
When a failure triggers retries or long-running steps like step.waitForEvent() are used, a Durable Endpoint redirects to a separate endpoint that waits for the call to finish.
By default, this is an endpoint either in the Inngest Dev Server or Inngest Cloud, depending on which environment you're in, but inngest.endpointProxy() can be used to create your own URL to satisfy CORS constraints when the endpoint is used from browsers.
typescript
// When setting the `endpointAdapter`, use `.withOptions()` to set more config
const inngest = new Inngest({
id: "my-app",
endpointAdapter: endpointAdapter.withOptions({
asyncRedirectUrl: "/wait",
}),
});
// Then create the route with `inngest.endpointProxy()`
Bun.serve({
port: 3000,
routes: {
"/process": ...,
"/wait": inngest.endpointProxy(),
},
});Requests will now be redirected to /wait.
To stream data to the client during execution, see streaming below or the full streaming guide.
Streaming
Durable Endpoints can stream data back to clients in real-time using Server-Sent Events (SSE). For a full guide covering concepts, examples, and rollback semantics, see the guide.
Server: stream.push() and stream.pipe()
Import the stream object from inngest/experimental/durable-endpoints and use it inside your endpoint handler within a step.run() call:
ts
import { step } from "inngest";
import { stream } from "inngest/experimental/durable-endpoints";stream.push((data: unknown) => void): Send a single chunk of data to the client as an SSE event. Accepts any JSON-serializable value. Does not block execution. No-op outside of an Inngest execution context.
stream.pipe((source: ReadableStream | AsyncIterable<string> | (() => AsyncIterable<string>)) => Promise<string>): Pipe a stream source to the client, sending each chunk as an SSE event in real-time. Resolves with the concatenated text of all chunks. No-op outside of an Inngest execution context (resolves with an empty string).
Client: fetchWithStream()
Handles the implementation details of streaming, including:
- Automatically committing and rolling back chunks based on the step's success or failure
- Filtering out internal events
- Handling the sync-to-async redirect
ts
import { fetchWithStream } from "inngest/experimental/durable-endpoints/client";Returns a Promise<Response> containing the endpoint's final return value. Sync-to-async redirects are handled automatically. If the endpoint does not use streaming, the raw Response is returned as-is.
url(string): The URL of the Durable Endpoint to call.
fetch(typeof fetch): Custom fetch implementation. Defaults to globalThis.fetch.
fetchOpts(RequestInit): Options passed to the underlying fetch call (e.g. { signal } for cancellation).
onMetadata((args: { runId: string }) => void): Called when run metadata is received. Always fires first.
onData((args: { data: unknown; hashedStepId: string | null }) => void): Called for each streamed chunk. Data should be considered uncommitted until onCommit fires.
onCommit((args: { hashedStepId: string | null }) => void): Called when a step completes successfully. Chunks from that step are now permanent.
onRollback((args: { hashedStepId: string | null }) => void): Called when a step fails and will retry. Discard uncommitted chunks from that step.
Node.js Utilities
The inngest/node entry point exports helpers for serving Durable Endpoints in Node.js environments:
ts
import { serveEndpoint, createEndpointServer } from "inngest/node";serveEndpoint((handler: (req: Request) => Promise<Response>) => http.RequestListener): Bridge a Web API endpoint handler to a Node.js http.RequestListener. Converts an incoming http.IncomingMessage into a Web API Request, invokes the handler, then streams the resulting Response back through the Node.js http.ServerResponse.
createEndpointServer((handler: (req: Request) => Promise<Response>) => http.Server): Create an http.Server that serves a Durable Endpoint handler directly. A convenience wrapper around serveEndpoint().
ts
import { createEndpointServer } from "inngest/node";
const server = createEndpointServer(
inngest.endpoint(async (req) => {
const result = await step.run("work", async () => {
return await doWork();
});
return Response.json({ result });
})
);
server.listen(3000);