Durable Endpoints Beta
Create durable HTTP endpoints using inngest.endpoint(). Each step within the handler is checkpointed, allowing automatic recovery from failures.
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:
import { Inngest } from "inngest";
import { endpointAdapter } from "inngest/edge";
const inngest = new Inngest({
id: "my-app",
endpointAdapter,
});
The endpointAdapter is required. Without it, inngest.endpoint() will not be available.
endpointAdapter.withOptions(options)
Use withOptions() to customize adapter behavior:
const inngest = new Inngest({
id: "my-app",
endpointAdapter: endpointAdapter.withOptions({
asyncRedirectUrl: "/api/inngest/poll",
retries: 5,
}),
});
- Name
asyncRedirectUrl- Type
- string | ((params: { runId: string; token: string }) => string | Promise<string>)
- Required
- optional
- Description
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
runIdandtokenquery parameters. A function gives you full control over URL construction.
- Name
functionId- Type
- string
- Required
- optional
- Description
Override the auto-detected function ID for this endpoint. Defaults to
{METHOD} {path}.
- Name
retries- Type
- number
- Required
- optional
- Description
Maximum retries for all steps in this endpoint. Must be between
0and20. Defaults to3.
- Name
asyncResponse- Type
- "redirect" | "token"
- Required
- optional
- Description
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.
- Name
handler- Type
- (...args: any[]) => any
- Required
- required
- Description
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.
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.
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.
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.
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
POST body is not yet supported. Use query string parameters to pass data to Durable Endpoints. POST body support is coming soon.
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:
// 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:
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.
// 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 Developer Preview
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:
import { step } from "inngest";
import { stream } from "inngest/experimental/durable-endpoints";
- Name
stream.push- Type
- (data: unknown) => void
- Required
- optional
- Description
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.
- Name
stream.pipe- Type
- (source: ReadableStream | AsyncIterable<string> | (() => AsyncIterable<string>)) => Promise<string>
- Required
- optional
- Description
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
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.
- Name
url- Type
- string
- Required
- required
- Description
The URL of the Durable Endpoint to call.
- Name
fetch- Type
- typeof fetch
- Required
- optional
- Description
Custom fetch implementation. Defaults to
globalThis.fetch.
- Name
fetchOpts- Type
- RequestInit
- Required
- optional
- Description
Options passed to the underlying
fetchcall (e.g.{ signal }for cancellation).
- Name
onMetadata- Type
- (args: { runId: string }) => void
- Required
- optional
- Description
Called when run metadata is received. Always fires first.
- Name
onData- Type
- (args: { data: unknown; hashedStepId: string | null }) => void
- Required
- optional
- Description
Called for each streamed chunk. Data should be considered uncommitted until
onCommitfires.
- Name
onCommit- Type
- (args: { hashedStepId: string | null }) => void
- Required
- optional
- Description
Called when a step completes successfully. Chunks from that step are now permanent.
- Name
onRollback- Type
- (args: { hashedStepId: string | null }) => void
- Required
- optional
- Description
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:
import { serveEndpoint, createEndpointServer } from "inngest/node";
- Name
serveEndpoint- Type
- (handler: (req: Request) => Promise<Response>) => http.RequestListener
- Required
- optional
- Description
Bridge a Web API endpoint handler to a Node.js
http.RequestListener. Converts an incominghttp.IncomingMessageinto a Web APIRequest, invokes the handler, then streams the resultingResponseback through the Node.jshttp.ServerResponse.
- Name
createEndpointServer- Type
- (handler: (req: Request) => Promise<Response>) => http.Server
- Required
- optional
- Description
Create an
http.Serverthat serves a Durable Endpoint handler directly. A convenience wrapper aroundserveEndpoint().
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);