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 for Durable Endpoints to function. 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 runId and token query 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 0 and 20. Defaults to 3.

  • 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.

const inngest = new Inngest({
  id: "my-app",
  endpointAdapter: endpointAdapter.withOptions({
    asyncRedirectUrl: "/wait",
  }),
});

// Create the proxy 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 SSE guide.


Streaming Developer Preview

Durable Endpoints can stream data back to clients in real-time using Server-Sent Events (SSE). This lets you stream AI inference tokens, progress updates, or any other data while keeping durability guarantees. If you don't need to stream data directly to an HTTP client, consider using Realtime to push updates via pub/sub channels instead.

For a full guide covering concepts, examples, and rollback semantics, see Streaming SSE from Durable Endpoints.

Server: stream.push() and stream.pipe()

Import the stream object from inngest/experimental/durable-endpoints and use it inside your endpoint handler, typically 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. Fire-and-forget — does not block execution. No-op outside of an Inngest execution context.

await step.run("process", async () => {
  stream.push("Loading...");
  stream.push({ progress: 50 });
  const result = await doWork();
  stream.push("Done!");
  return result;
});
  • Name
    stream.pipe
    Type
    (source: ReadableStream | AsyncIterable<string> | (() => AsyncIterable<string>)) => Promise<string>
    Required
    optional
    Description

    Pipe a stream to the client, sending each chunk as an SSE event in real-time. Resolves with the concatenated text of all chunks — it both streams to the client and collects the result for you.

    Accepts three source types:

    • ReadableStream — piped directly, decoded from bytes to string chunks.
    • AsyncIterable<string> — each yielded value becomes a chunk.
    • () => AsyncIterable<string> — a factory function, letting you pass async function* generators directly.

    Outside of an Inngest execution context, resolves with an empty string.

await step.run("generate", async () => {
  // Pipe a ReadableStream
  const res = await fetch("https://api.example.com/stream");
  const text = await stream.pipe(res.body);

  // Or pipe an async generator
  const text = await stream.pipe(async function* () {
    for await (const event of llmStream) {
      if (event.type === "content_block_delta") {
        yield event.delta.text;
      }
    }
  });
});

Client: fetchWithStream()

Import from inngest/experimental/durable-endpoints/client to consume a streaming Durable Endpoint:

import { fetchWithStream } from "inngest/experimental/durable-endpoints/client";

Returns a Promise<Response>. The returned Response contains the endpoint's final return value. If the endpoint does not use streaming, the raw Response is returned as-is. Sync-to-async redirects are handled automatically.

  • 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 fetch call (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. Each stream.push() or stream.pipe() yield produces one onData call. data is the deserialized value; hashedStepId identifies which step produced it (or null if streamed outside a step). Data should be considered uncommitted until onCommit fires.

  • Name
    onCommit
    Type
    (args: { hashedStepId: string | null }) => void
    Required
    optional
    Description

    Called when a step completes successfully. Chunks from that step are now permanent and will never be rolled back.

  • Name
    onRollback
    Type
    (args: { hashedStepId: string | null }) => void
    Required
    optional
    Description

    Called when a step fails and will retry. Your code is responsible for discarding the uncommitted chunks from that step.

  • Name
    onStreamError
    Type
    (error: string) => void
    Required
    optional
    Description

    Called on terminal stream errors (permanent function failure, network error, etc.).

  • Name
    onDone
    Type
    () => void
    Required
    optional
    Description

    Called when the stream is fully consumed (including on abort or error).

const resp = await fetchWithStream("/api/generate", {
  onData: ({ data }) => {
    if (typeof data === "string") {
      console.log("Chunk:", data);
    }
  },
  onCommit: () => {
    // Chunks are permanent
  },
  onRollback: () => {
    // Discard uncommitted chunks
  },
  onStreamError: (error) => {
    console.error("Stream error:", error);
  },
});

const result = await resp.text();

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 incoming http.IncomingMessage into a Web API Request, invokes the handler, then streams the resulting Response back through the Node.js http.ServerResponse.

  • Name
    createEndpointServer
    Type
    (handler: (req: Request) => Promise<Response>) => http.Server
    Required
    optional
    Description

    Create an http.Server that serves a Durable Endpoint handler directly. A convenience wrapper around serveEndpoint().

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);